在 C++ 庞大语法体系中, using 关键字十分的灵活多用,它可不简单。
除了常规的引入命名空间之外,它还可用于引入枚举类型枚举器、定义常规类型别名、模板类型别名等。在定义常规类型别名方面与C语言中的typedef、#define
与之相似,但又不尽相同。
简单归纳起来, using 的主要作用是 “引入” 和 “定义别名”。
引入
引入命名空间
using namespace ns-name ; |
引入命名空间是最常见也最好理解的用法,如:
#include <iostream>
using namespace std;
int main() {
cout << "Hello World!" << endl;
// std::cout << "Hello World!" << endl;
return 0;
}
在这个示例中,我们使用 using namespace
将命名空间 std
的全部成员引入到当前命名空间(根空间)中。
这样做的好处是,代码变得简洁,有效作用域内,都可以省略命名空间,直接访问。
有效作用域是指从 using 声明点开始,直到包含该using声明的作用域结尾。
using 关键字可以引入整个命名空间或者特定的命名空间成员。
该怎样直观的理解这句话呢 ,看如下代码:
namespace
{
int i; // 定义 ::(unique)::i
}
void f()
{
i++; // 定义 ::(unique)::i
}
namespace A
{
namespace
{
int i; // A::(unique)::i
int j; // A::(unique)::j
}
void g() { i++; } // A::(unique)::i++
}
using namespace A; // 将 A 中的所有名称引入全局名称空间
void h()
{
i++; // error: ::(unique)::i and ::A::(unique)::i 都在作用域中
A::i++; // ok, 递增 ::A::(unique)::i
j++; // ok, 递增 ::A::(unique)::j
}
由例子可见,因为引入了命名空间A,其下的所有成员都将暴露出来。变量 i 在两个命名空间中都有声明,因此产生了冲突。
因此,在.h头文件中,一般不应该使用using声明,避免产生名字冲突。
方便的同时,也带来了问题,如命名空间污染。
#include <iostream>
#include <utility>
using namespace std;
void move(int &&x) {
cout << "Move: " << x << endl;
}
int main() {
move(10); // 调用 void move(int &&x);
std::move(10); // 调用 std::move
return 0;
}
在这个示例中 move(10)
会调用本源文件的 move
,也许这并不是你的预期。当然,我们依然可以使用 std::move
显式调用。
💡 命名空间污染是十分隐晦和危险的,特别是当多个命名空间中有相同名称的成员时,需要特别的注意。
另外,这里还有一个“后续扩展成员不可见的”规则,即在 using 后访问的命名空间,无法访问到后续扩展命名空间时新增的成员,有说起来点绕,看示例:
namespace A
{
int i;
}
int main()
{
A::i++; // ok
A::j++; // error
}
namespace A
{
int j;
}
i 在 using 之前就定义了,可以正常访问, j 在using、访问之后定义,不可见。
由此规则,可能会衍生一个潜在的混淆:
namespace A
{
void f(int) {}; // 定义一个接受int参数的函数f
}
using A::f; // 使得全局命名空间中的::f成为A::f(int)的同义词
// 扩展命名空间A
namespace A
{
void f(char) {}; // 定义一个接受char参数的函数f,不改变全局命名空间中::f的含义
}
int main()
{
f('a'); // 调用 void f(int),参数隐式转换
using A::f; // 重新引入完整的命名空间A
f('a'); // 调用 void f(char)
}
一个例外是函数模板特化:如果引入的是类模板,后续添加的特化版本是可见的,因为它们的查找是通过主模板进行的。
引入命名空间的实体
using ns-name :: member-name ; |
相对于引入整个命名空间,引入命名空间内的实体操作更为细腻,如:
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int main() {
cout << "Hello World!" << endl;
// std::cout << "Hello World!" << endl;
return 0;
}
在这个示例中,我们只引入了 std命名空间中的cin、cout、endl 实体成员。当然这也仅仅是降低了命名空间污染的可能性,最终,需要我们自己审慎把握。
💡 从 C++ 17 开始,允许使用
,
分隔,同时引入多个命名空间成员。using std::cin, std::cout, std::endl;
引入类的继承体系
using classscope::member; |
首先,在类的继承体系中,using 使得派生类显式地重新暴露基类的被隐藏或覆盖的成员函数,使得基类的成员函数在派生类的作用域内重新可用。
class Base {
public:
virtual void foo() { /*...*/ }
void bar() {}
};
class Derived : public Base {
public:
void foo() override { /*...*/ } // 重写foo
using Base::foo; // 显式再次引入Base的foo版本
void bar() {}
using Base::bar;
};
int main()
{
Derived d;
d.foo(); // 调用 Derived::foo();
d.Base::foo(); // 调用被覆盖的 Base::foo();
d.bar(); // 调用 Derived::bar();
d.Base::bar(); // 调用被隐藏的 Base::bar();
}
其次,在子类私有继承时,可以改变成员的可见性,如:
class Base {
public:
void foo() {}
void bar() {}
};
class Derived : private Base {
public:
using Base::foo; // 显式引入Base的foo
//using Base::bar; // 未引入Base的bar
};
int main()
{
Derived d;
d.foo(); // Base::foo() 可访问
d.bar(); // Base::bar() 可访问
}
继承构造函数 (C++11)
派生类可以使用 using 声明从直接基类继承构造函数
在C++11之前,如果基类有一个构造函数需要参数,那么在派生类中必须显式地调用这个构造函数,当基类的构造函数参数很多、版本很多的时候,这将是一件繁重、容易出错的工作。
现在,使用 using 即可全部引入。
#include <iostream>
using namespace std;
class Base
{
public:
Base() { cout << "Base()" << endl; }
Base(const Base& other) { cout << "Base(Base&)" << endl; }
explicit Base(int i) : num(i) { cout << "Base(int)" << endl; }
explicit Base(char c) : letter(c) { cout << "Base(char)" << endl; }
private:
int num;
char letter;
};
class Derived : Base
{
public:
// 继承基类所有构造函数
using Base::Base;
private:
// 不能从构造函数初始化成员,需要手动初始化
int newMember{ 0 };
};
int main()
{
cout << "Derived d1(5) calls: ";
Derived d1(5);
cout << "Derived d1('c') calls: ";
Derived d2('c');
cout << "Derived d3 = d2 calls: " ;
Derived d3 = d2;
cout << "Derived d4 calls: ";
Derived d4;
}
观察输出
Derived d1(5) calls: Base(int)
Derived d1('c') calls: Base(char)
Derived d3 = d2 calls: Base(Base&)
Derived d4 calls: Base()
这种方式大幅减轻了继承基类构造函数的书写。另外,由于这是隐式声明继承的,假设一个继承构造函数不被相关的代码使用,编译器不会为之产生真正的函数代码,更加节省目标代码空间。
缺憾就是不能方便的同时初始化成员,需要手动初始化。
💡 只能引入直接基类的构造函数,例如
class DD : public Derived { using Base::Base; }
是非法的。
引入枚举类型(C++20)
可以通过 using 引入到枚举类型枚举器名称,效果类似于每个枚举器定义在了作用域中,如
enum class fruit { orange, apple };
struct S
{
using enum fruit; // OK: 将 orange 和 apple 引入 S
};
void f()
{
S s;
s.orange; // OK: fruit::orange
S::orange; // OK: fruit::orange
}
可以通过 using多次引入多个枚举类型,但如果枚举器名称有重复将产生冲突:
enum class fruit { orange, apple };
enum class color { red, orange };
void f()
{
using enum fruit; // OK
// using enum color; // error: color::orange and fruit::orange conflict
}
定义别名
定义类型别名
using alias = typename; |
在 C/C++ 中,定义类型别名的方法有 #define、
typedef、
using 多种方式。
#define 是宏定义关键字,用途广法,尤其是 C 中,不局限于类型别名定义。
在C++中,推荐使用 using 来定义类型别名,因为它更符合C++的现代编程风格。
首先,直观的感受下三者在定义类型别名时的形式,typedef 的方式和其他两者顺序相反:
#define MY_INT int
typedef int MyInt;
using MyInt = int;
似乎三者没有明显区别,当然由于 #define 宏定义的本质,和后两者是可以明显区分开的。那么,typedef 和
using 的区别在哪里呢?
首先,使用typedef定义的别名和使用using定义的别名在语义上是等效的。
然后,我们看下一个定义类型别名的例子:
typedef std::unique_ptr<std::unordered_map<std::string, std::string>> UPtrMapSS;
using UPtrMap = std::unique_ptr<std::unordered_map<std::string, std::string>>;
using 的方式是不是更为直观易懂?
当然,也许你很习惯 typedef 的方式,这个示例还不足以让你倒向 using ,那我们继续。
定义模板类型别名
typedef 是不支持定义模板类型别名的,例如
template <typename T>
typedef map<int, T> type; // error, 语法错误
要实现这一点,需要一个类辅助,这样就很麻烦了。using 可以做到:
template <typename T>
using mymap = map<int, T>;
typedef 为什么不可以呢,在 n1449 中有这样一段话:
"we specifically avoid the term “typedef template” and introduce the new syntax involving the pair “using” and “=” to help avoid confusion: we are not defining any types here, we are introducing a synonym (i.e. alias) for an abstraction of a type-id (i.e. type expression) involving template parameters."
所以,这事标准委员会的观点与选择,涉及到模板类型时,我们必须使用 using 。
总结
using 关键字为程序书写、定义别名带来了极大的方便,使得代码更为简练,善用 using ,使得代码更为简洁易懂。
在 C++11 后续的语言标准中又增加了更多的适用场景如,变量模板(C++14)、类模板的默认实参和推导实参(C++17)、委托转隶构造函数(使用using
)(C++20),最后以一段 特性大杂烩作为结束
// 类型别名声明 (C++11)
using IntVector = std::vector<int>;
// 模板别名 (C++11)
template <typename T>
using Pair = std::pair<T, T>;
// 委托构造函数 (C++11)
class Foo {
public:
using FooBase::FooBase; // 委托给基类的构造函数
};
// 继承构造函数 (C++11)
class Base {
public:
Base(int i) {}
};
class Derived : public Base {
public:
using Base::Base; // 使用基类的构造函数
};
// 构造函数的显式使用声明 (C++11)
struct A {
A(int) {}
};
struct B : A {
using A::A; // 显式使用A的构造函数
};
// 变量模板 (C++14)
template<typename T>
T template_var = 10;
// 类模板的默认实参和推导实参 (C++17)
template <typename T = int>
struct C {};
// 委托转隶构造函数 (C++20)
struct D : B {
using B::B; // 委托给另一个构造函数
};
参考
C++ keyword: using - cppreference.com
Constructors (C++) | Microsoft Learn
http://isocpp.open-std.org/JTC1/SC22/WG21/docs/papers/2003/n1449.pdf