c++11 using语义的扩展及汇总

publish time : 2021-02-03T23:30:00+08:00


1. 内容概要

  自c++11开始“using”关键字的语义被扩展为3种,分别是:

  • 使用指令(using-directive):c++11以前就有的;

  • 使用声明(using-declaration):c++11新增的;

  • 别名声明(alias-declaration):c++11新增的;

  本文中部分案例涉及标识符隐藏问题,如有需要,欢迎浏览我的另一篇博文《c++标识符的隐藏与歧义现象》


2. 语义说明

  • 使用指令(using-directive):将指定命名空间中的全部标识符导入当前作用域,该语义及语法在c++11中没有改变;

  • 使用声明(using-declaration):将指定标识符在当前作用域中重新声明;需要注意的是:

    • 与使用指令不同,使用声明视同在当前作用域中直接声明,所以如果当前作用域中存在同名标识符将因命名冲突(name conflict)而无法通过编译(重载函数标识符除外);也可以将此处的命名冲突理解为重复声明(redeclaration);

    • 若使用声明的目标是重载函数的标识符,将重新声明全部的重载函数而不是仅声明其中一个;

  • 别名声明(alias-declaration):为目标类型取别名;含义与typedef相同,但是对于需要保留范式的类型,使用using比typedef更方便。c++11中,别名声明的功能涵盖了typedef。


3. 使用指令

// @brief 使用指令-基本语法示例
// @author Qiujun Lv
// @date 2021-02-03T22:30:00+08:00
// @standard c++11
// @compile 代码的测试编译器列表:X86-64 gcc 10.1、X86-64 clang 11.0.0、
//   X86-64 msvc v19.27、X86 msvc v19.27、mingw 73_64、mingw 73_32、
//   ARM64 gcc 6.4、ARM gcc 6.4、 ARM64 gcc 6.3.0(linux)
#include <iostream>
int main(){
  //将命名空间std中的全部声明/定义导入当前作用域
  using namespace std ;
  //命名空间“std”中的“std::cout”、“std::endl” 都可以直接使用了
  cout << 123 << endl ;
  return 0;
}

4. 使用声明

4.1 使用声明-例1-基本语法

// @brief 使用声明-例1-基本语法
// @author Qiujun Lv
// @date 2021-02-03T23:30:00+08:00
// @standard c++11
// @compile 代码的测试编译器列表:X86-64 gcc 10.1、X86-64 clang 11.0.0、
//   X86-64 msvc v19.27、X86 msvc v19.27、mingw 73_64、mingw 73_32、
//   ARM64 gcc 6.4、ARM gcc 6.4、 ARM64 gcc 6.3.0(linux)
#include <iostream>
int main(){
  //将命名空间std中的cout在当前作用域中重新声明
  using std::cout ;
  //命名空间“std”中的“std::cout”可以直接使用了
  //但“std”中“std::endl” 及其他标识符仍然需要通过作用域限定符访问
  cout << 123 << std::endl ;
  return 0;
}

4.2 使用声明-例2-引发命名冲突

  由于使用声明视同在当前作用域中直接声明,所以存在引发命名冲突的可能性。相对地,使用指令不会引发命名冲突,若有同名只会优先级低的标识符被隐藏。

// @brief 使用声明-例2-引发命名冲突
// @author Qiujun Lv
// @date 2021-02-03T23:30:00+08:00
// @standard c++11
// @compile 该代码需要修改才能通过编译
//   代码的测试编译器列表:X86-64 gcc 10.1、X86-64 clang 11.0.0、
//   X86-64 msvc v19.27、X86 msvc v19.27、mingw 73_64、mingw 73_32、
//   ARM64 gcc 6.4、ARM gcc 6.4、 ARM64 gcc 6.3.0(linux)
#include <iostream>
namespace package{
  std::string var = "package::var" ;
} 

int main(){
  //将命名空间package中的var在当前作用域中重新声明
  using package::var ;
  //编译失败:命名冲突,声明与使用声明引入的var发生冲突
  std::string var = "package::var" ;
  //解决方案A:为局部作用域或命名空间作用域中的var换个名字
  //解决方案B: 以使用指令导入命名空间,并以作用域限定符加以区分
  std::cout << package::var << std::endl ;
  return 0;
}

4.3 使用声明-例3-可见性提升

  还是因为使用声明视同在当前作用域中直接声明,在下面的例子中标识符的可见性被提升了。

// @brief 使用声明-例3-可见性提升
//   本例演示通过“使用声明”将当前位置可以观察到的元素开放
// @author Qiujun Lv
// @date 2021-02-03T23:30:00+08:00
// @standard c++11
// @compile 代码的测试编译器列表:X86-64 gcc 10.1、X86-64 clang 11.0.0、
//   X86-64 msvc v19.27、X86 msvc v19.27、mingw 73_64、mingw 73_32、
//   ARM64 gcc 6.4、ARM gcc 6.4、 ARM64 gcc 6.3.0(linux)
#include <iostream>
class ClassA{
protected://若此处使用private关键字,子类的using就行不通了,因为使用声明的前提是在当前位置可见
  int var ;
  ClassA () :var(0){ }
  void foo( ){ printf("ClassA::foo( ) \n"); }
};

//注意:此处为私有继承,正常情况下生成ClassB的对象后是无法通过对象访问ClassA的变量和方法的。
//此处通过“使用声明”实现了访问权的开放。
class ClassB : private ClassA{
public:
  using ClassA::var;//使用声明:父类变量的可见性被提升了
  using ClassA::foo;//使用声明:父类函数的可见性被提升了
};

int main()
{
  ClassB obj_b ;
  obj_b.foo();//访问被声明的函数
  std::cout << obj_b.var/*访问被声明的变量*/ << std::endl;
  return 0;
}

4.4 使用声明-例4-跨类型函数重载

  在c11之前,跨类型的函数重载是麻烦的,因为子类中的函数标识符会将基类中的全部同名函数标识符隐藏,下面的例子演示了该问题在c11中的解决方案。

// @brief 使用声明-例4-跨类型函数重载
// @author Qiujun Lv
// @date 2021-02-03T23:30:00+08:00
// @standard c++11
// @compile 代码的测试编译器列表:X86-64 gcc 10.1、X86-64 clang 11.0.0、
//   X86-64 msvc v19.27、X86 msvc v19.27、mingw 73_64、mingw 73_32、
//   ARM64 gcc 6.4、ARM gcc 6.4、 ARM64 gcc 6.3.0(linux)
#include <iostream>
class ClassB;
class ClassA{
public:
  virtual void foo(){ printf("ClassA::foo() \n"); }
  void foo(int){ printf("ClassA::foo(int) \n"); }
protected:
  //下行定义的函数是保护的,但在子类的“使用声明”行为导致可见性被提高了
  void foo(int,int){ printf("ClassA::foo(int,int) \n"); }
private:
  //若存在私有重载函数,则必须配合friend关键字,否则子类中的使用声明无法通过编译,
  //因为对于using-declaration来说,声明的目标必须是在当前作用域可见的。
  friend ClassB;
  void foo(int,int,int){ printf("ClassA::foo(int,int,int) \n"); }
};

class ClassB:public ClassA{
public:
 void foo() override { printf("ClassB::foo() \n"); }
  //使用声明,下面的语句协助实现父子两类间的函数重载
  //尽管在父类中存在的重载函数多于一个,但仅需声明一次
  using ClassA::foo;
};

int main()
{
  ClassB b;
  b.foo( );//ClassB::foo()
  b.foo( 1 );//ClassA::foo(int)
  b.foo( 1,2 );//ClassA::foo(int,int)
  b.foo( 1,2,3 );//ClassB::foo(int,int,int)
}

4.5 使用声明与使用指令的区别

  使用声明与使用指令有以下2点区别:

  • (1) 导入对象不同:使用指令导入的是整个命名空间的内容,而使用声明只导入指定的内容;

  • (2) 结果优先级不同:使用指令导入的内容优先级比当前作用域内固有的内容要低,而使用声明具有与当前作用域固有内容相同的优先级,使用声明行为视同在本作用域内声明。

  使用指令可以避免命名冲突,但可能会不必要地扩大导入的范围;使用声明可以只导入必要声明/定义,但可能引发冲突;使用时须稍加斟酌。


5. 别名声明

5.1 别名声明-基本语法

  为类型“std::int64_t”取了一个别名——“int64”:

using int64 = std::int64_t; 

  这等价于:

typedef std::int64_t int64; 

  又比如一些类型书写冗长的情况:

using seconds = std::chrono::seconds;

  为函数指针类型取别名:

#include <iostream>
// @brief 一个用于演示的函数
int plus(int one,int another){
 return one+another;
}

int main(){
  //声明函数指针的类型别名,等价于:typedef int (*fun_ptr_t) (int,int);
  using fun_ptr_t = int (*) (int,int);
  //定义指针
  fun_ptr_t fun_ptr = plus ;
  //通过指针调用函数
  std::cout<< fun_ptr(3,5) ;
  return 0 ;
}

  显而易见地,typedef的语法更具备一致性,与声明高度相似;而别名声明的语法可读性更强、更贴近常人的思维;请感受以下两行代码:

typedef int (*fun_ptr_t) (int,int);//一致性

using fun_ptr_t = int (*) (int,int);//可读性

  还有模板类型的别名:

//等价于typedef std::map<int,int> MapOfInt2Int;
using MapOfInt2Int = std::map<int,int> ;

  值得注意的是,此处无论使用using还是typedef,MapOfInt2Int的模板参数都已经被固定为<int,int>了;如果希望保留全部或部分模板特性,则需要使用“模板别名”。

5.2 别名声明-模板别名

  使用“模板别名”可以在为类型取别名的同时保留全部或部分模板特性:

#include <iostream>
#include <vector>
#include <string>

//模板别名
template<typename element_t>
using List = std::vector<element_t> ;

int main(){
  //生成&装载
  List<std::string> list;
  list.push_back(std::string("One"));
  list.push_back(std::string("Two"));
  //遍历
  for( auto& element : list ) std::cout << element << std::endl;
  return 0 ;
}

  c++11之前,如果要使用typedef实现同样的效果,则会略显繁琐:

#include <iostream>
#include <vector>
#include <string>

//typedef是不支持模板的,需要借助一个模板结构体/类对typedef语句进行包装
template<typename element_t>
struct TemplateWrapper{
    typedef std::vector<element_t> List ;
};

int main(){
  //生成&装载
  TemplateWrapper<std::string>::List list;
  list.push_back(std::string("One"));
  list.push_back(std::string("Two"));
  //遍历
  for( auto& element : list ) std::cout << element << std::endl;
  return 0 ;
}

6. 结束语

  本文简单讲解了c++11中using的三种语义及语法,如有不够详尽乃至谬误之处,还望不吝斧正。
  语义和语法只是基础,实践过程中的语用才是关键,就好像我明明掌握中文却写不出红楼梦一样。
  最后,感谢阅读,江湖再会。


本文章使用limfx的vscode插件快速发布