在 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)::iint 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)::ij++; // 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::movereturn 0;
}
在这个示例中 move(10)
会调用本源文件的 move
,也许这并不是你的预期。当然,我们依然可以使用 std::move
显式调用。
💡 命名空间污染是十分隐晦和危险的,特别是当多个命名空间中有相同名称的成员时,需要特别的注意。
另外,这里还有一个“后续扩展成员不可见的”规则,即在 using 后访问的命名空间,无法访问到后续扩展命名空间时新增的成员,有说起来点绕,看示例:
namespace A
{int i;
}int main()
{A::i++; // okA::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; // 重新引入完整的命名空间Af('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 { /*...*/ } // 重写foousing 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::orangeS::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 inttypedef 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