using 声明

来自cppreference.com
< cpp‎ | language

将别处定义的名字引入到此 using 声明所出现的声明区中。

using typename(可选) 嵌套名说明符 无限定标识 ; (C++17 前)
using 声明符列表 ; (C++17 起)
typename - 在 using 声明从基类向类模板中引入成员类型时,可能需要用关键词 typename 解决待决名
嵌套名说明符 - 名字与作用域解析运算符 :: 的序列,以作用域解析运算符结尾。单个 :: 指代全局命名空间。
无限定标识 - 标识表达式
声明符列表 - 一个或多个形式为 typename(可选) 嵌套名说明符 无限定标识 的声明符的逗号分隔列表。某些或所有声明符都可以后随省略号 ... 以指示包展开

解释

using 声明可以用于将命名空间成员引入到另一命名空间与块作用域,或将基类成员引入到派生类定义中,或将枚举项引入命名空间、块或类作用域中 (C++20 起)

包含多于一个 using 声明符的 using 声明等价于一个对应个数的包含单个 using 声明符的 using 声明序列。

(C++17 起)

在命名空间和块作用域中

using 声明将另一命名空间的成员引入到当前命名空间或块作用域中。

#include <iostream>
#include <string>
using std::string;
int main()
{
    string str = "举例";
    using std::cout;
    cout << str;
}

细节见命名空间

在类定义中

using 声明可以将基类成员引入到派生类的定义中,例如将基类的受保护成员暴露为派生类的公开成员。此时 嵌套名说明符 必须指名所定义的类的某个基类。如果这个名字是该基类的某个重载的成员函数的名字,那么具有该名字的所有基类成员函数均被引入。如果派生类已包含具有相同名字、形参列表和限定的成员,那么派生类成员隐藏或覆盖从基类引入的成员(不与之冲突)。

#include <iostream>
struct B {
    virtual void f(int) { std::cout << "B::f\n"; }
    void g(char)        { std::cout << "B::g\n"; }
    void h(int)         { std::cout << "B::h\n"; }
 protected:
    int m; // B::m 是受保护的
    typedef int value_type;
};
 
struct D : B {
    using B::m; // D::m 是公开的
    using B::value_type; // D::value_type 是公开的
 
    using B::f;
    void f(int) { std::cout << "D::f\n"; } // D::f(int) 覆盖 B::f(int)
    using B::g;
    void g(int) { std::cout << "D::g\n"; } // g(int) 与 g(char) 均作为 D 成员可见
    using B::h;
    void h(int) { std::cout << "D::h\n"; } // D::h(int) 隐藏 B::h(int)
};
 
int main()
{
    D d;
    B& b = d;
 
//    b.m = 2; // 错误,B::m 受保护
    d.m = 1; // 受保护的 B::m 可作为公开的 D::m 访问
    b.f(1); // 调用派生类 f()
    d.f(1); // 调用派生类 f()
    d.g(1); // 调用派生类 g(int)
    d.g('a'); // 调用基类 g(char)
    b.h(1); // 调用基类 h()
    d.h(1); // 调用派生类 h()
}

输出:

D::f
D::f
D::g
B::g
B::h
D::h

继承构造函数

如果 using 声明指代了正在定义的类的某个直接基类的构造函数(例如 using Base::Base;),那么在初始化派生类时,该基类的所有构造函数(忽略成员访问)均对重载决议可见。

如果重载决议选择了继承的构造函数,那么如果它被用于构造相应基类的对象时可访问,它也是可访问的:引入它的 using 声明的可访问性被忽略。

如果在初始化这种派生类对象时重载决议选择了继承的构造函数之一,那么用这个继承的构造函数对从之继承该构造函数的 Base 子对象进行初始化,而 Derived 的所有其他基类和成员,都如同以预置的默认构造函数一样进行初始化(如果提供默认成员初始化器则使用,否则进行默认初始化)。整个初始化被视作单个函数调用:继承的构造函数的各形参的初始化,按顺序早于派生类对象的任何基类或成员的初始化。

struct B1 {  B1(int, ...) { } };
struct B2 {  B2(double)   { } };
 
int get();
 
struct D1 : B1 {
  using B1::B1;  // 继承 B1(int, ...)
  int x;
  int y = get();
};
 
void test() {
  D1 d(2, 3, 4); // OK:B1 通过调用 B1(2, 3, 4) 初始化,
                 // 然后 d.x 被默认初始化(不进行初始化),
                 // 然后 d.y 通过调用 get() 初始化
  D1 e;          // 错误:D1 没有默认构造函数
}
 
struct D2 : B2 {
  using B2::B2; // 继承 B2(double)
  B1 b;
};
 
D2 f(1.0);       // 错误:B1 没有默认构造函数
struct W { W(int); };
struct X : virtual W {
 using W::W;   // 继承 W(int)
 X() = delete;
};
struct Y : X {
 using X::X;
};
struct Z : Y, virtual W {
  using Y::Y;
};
Z z(0); // OK:Y 的初始化不调用 X 的默认构造函数

如果构造函数从 B 类型的多个基类子对象继承,那么程序非良构,这与多继承的非静态成员函数相似:

struct A { A(int); };
struct B : A { using A::A; };
struct C1 : B { using B::B; };
struct C2 : B { using B::B; };
 
struct D1 : C1, C2 {
  using C1::C1;
  using C2::C2;
};
D1 d1(0); // 非良构:从不同的 B 基类子对象继承的构造函数
 
struct V1 : virtual B { using B::B; };
struct V2 : virtual B { using B::B; };
 
struct D2 : V1, V2 {
  using V1::V1;
  using V2::V2;
};
D2 d2(0); // OK:只有一个 B 子对象。
          // 这初始化虚 B 基类,它初始化 A 基类
          // 然后如同用预置的默认构造函数
          // 初始化 V1 与 V2 基类

与任何其他非静态成员函数的 using 声明相同,如果继承的构造函数的签名与 Derived 的构造函数之一匹配,那么它被 Derived 中找到的版本从查找中隐藏。如果 Base 的继承构造函数恰好有与 Derived 的复制/移动构造函数匹配的签名,那么它不妨碍 Derived 复制/移动构造函数的隐式生成(然后继承的版本被其隐藏,这与 using operator= 类似)。

struct B1 {   B1(int); };
struct B2 {   B2(int); };
 
struct D2 : B1, B2 {
  using B1::B1;
  using B2::B2;
  D2(int);   // OK:D2::D2(int) 隐藏 B1::B1(int) 和 B2::B2(int)
};
D2 d2(0);    // 调用 D2::D2(int)

模板化类中,如果 using 声明指代待决名,而 嵌套名说明符 拥有与该 无限定标识 相同的终止名,那么认为它指名构造函数。

template<class T>
struct A : T {
    using T::T; // OK :继承 T 的构造函数
};
 
template<class T, class U>
struct B : T, A<U> {
    using A<U>::A; // OK :继承 A<U> 的构造函数
    using T::A;    // 不继承 T 的构造函数尽管 T 可以是 A<> 的特化
};
(C++11 起)

引入有作用域枚举项

除了另一命名空间的成员和基类的成员, using 声明也能将枚举的枚举项引入命名空间、块和类作用域。

using 声明也能用于无作用域枚举项。

enum class button { up, down };
struct S {
    using button::up;
    button b = up; // OK
};
 
using button::down;
constexpr button non_up = down; // OK
 
constexpr auto get_button(bool is_up)
{
    using button::up, button::down;
    return is_up ? up : down; // OK
}
 
enum unscoped { val };
using unscoped::val; // OK ,尽管不需要
(C++20 起)

注解

只有在 using 声明中明确提及的名字才被传送到声明区中:特别是,using 声明枚举类型名时,并不传送其枚举项。

using 声明不能指代命名空间,有作用域枚举项 (C++20 前),基类的析构函数,或用户定义转换函数的成员模板的特化。

using 声明不能指名成员模板的特化(语法不容许模板标识):

struct B { template<class T> void f(); };
struct D : B {
      using B::f;      // OK:指名模板
//    using B::f<int>; // 错误:指名模板特化
      void g() { f<int>(); }
};

using 声明也不能引入待决的成员模板的名字作为模板名(不容许待决名template 消歧义符):

template<class X> struct B { template<class T> void f(T); };
template<class Y> struct D : B<Y> {
//  using B<Y>::template f; // 错误:不允许消歧义符
  using B<Y>::f;            // 能编译,但 f 不是模板名
  void g() {
//    f<int>(0);            // 错误:已知 f 不是目标名,
                            // 因此 < 不开始模板实参列表
      f(0);                 // OK
  }
};

如果 using 声明引入基类的赋值运算符到派生类,而其签名恰好与派生类的复制赋值或移动赋值运算符匹配,那么该运算符被派生类的隐式声明的复制/移动赋值运算符隐藏。相同的规则适用于继承恰好与派生类的移动/复制构造函数匹配的基类构造函数的 using 声明。 (C++11 起)

一项针对 C++11 的缺陷报告追溯地更改了继承构造函数的语义。以前,继承构造函数的声明导致将一组合成的构造函数声明注入到派生类中,这导致冗余的实参复制/移动,并与某些形式的 SFINAE 的交互有问题,而且某些情况下在主流 ABI 上无法实现。旧编译器可能仍然实现先前的语义。

旧的继承构造函数语义

如果 using 声明指代被定义类的某个直接基类的构造函数(例如 using Base::Base;),那么按照下列规则继承该基类的构造函数:

1) 继承构造函数候选的集合由以下组成:
a) 基类的所有非模板构造函数(在忽略省略号形参后,若它存在) (C++14 起)
b) 对于每个有默认实参或省略号形参的构造函数,所有构造函数签名通过丢弃省略号及逐个忽略来自形参列表尾的默认实参组成
c) 基类的所有构造函数模板(在忽略省略号形参数后,若它存在) (C++14 起)
d) 对于每个有默认实参或省略号形参的构造函数模板,所有构造函数签名通过丢弃省略号者及逐个忽略来自形参列表尾的默认实参组成
2) 所有并非默认构造函数或复制/移动构造函数,且其签名不与派生类中的用户定义的构造函数相匹配的继承构造函数候选,在派生类中隐式声明。不继承默认实参:
struct B1 {
    B1(int);
};
struct D1 : B1 {
    using B1::B1;
// 继承构造函数候选集是 
// 1. B1(const B1&)
// 2. B1(B1&&)
// 3. B1(int)
 
// D1 有下列构造函数:
// 1. D1() = delete
// 2. D1(const D1&) 
// 3. D1(D1&&)
// 4. D1(int) <- 继承的
};
 
struct B2 {
    B2(int = 13, int = 42);
};
struct D2 : B2 {
    using B2::B2;
// 继承构造函数候选集是
// 1. B2(const B2&)
// 2. B2(B2&&)
// 3. B2(int = 13, int = 42)
// 4. B2(int = 13)
// 5. B2()
 
// D2 有下列构造函数:
// 1. D2()
// 2. D2(const D2&)
// 3. D2(D2&&)
// 4. D2(int, int) <- 继承的
// 5. D2(int) <- 继承的
};

继承的构造函数等价于有空函数体和由单个 嵌套名说明符 组成的成员初始化器列表的用户定义构造函数,它将其所有实参转发给基类构造函数。

它与对应的基类构造函数有同样的访问。如果用户定义构造函数可满足 constexpr 构造函数的要求,那么它也是 constexpr 的。如果基类构造函数被弃置,或若预置的默认构造会被弃置,那么它也会被弃置(但不计构造函数要被继承的基类的构造)。继承的构造函数不能被显式实例化或显式特化。

如果两条 using 声明(分别从两个直接基类)继承了有相同签名的构造函数,那么程序非良构。

(C++11 起)

在 using 声明中使用包展开可以替代递归来组成暴露变长基类的重载成员的类:

template <typename... Ts>
struct Overloader : Ts... {
    using Ts::operator()...; // 从每个基类暴露 operator()
};
 
template <typename... T>
Overloader(T...) -> Overloader<T...>; // C++17 推导指引,C++20 起不再需要
 
int main() {
    auto o = Overloader{ [] (auto const& a) {std::cout << a;},
                         [] (float f) {std::cout << std::setprecision(3) << f;} };
}
(C++17 起)

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

DR 应用于 出版时的行为 正确行为
P0136R1 C++11 继承构造函数声明会将额外的构造函数注入派生类 导致名字查找能找到基类构造函数