using声明扩展之后可以支持逗号分隔的名称列表,这个特性也可以用于参数包。
例如,你现在可以这么写:
class Base {
public:
void a();
void b();
void c();
};
class Derived : private Base {
public:
using Base::a, Base::b, Base::c;
};
在C++17之前,你需要使用3个using声明分别进行声明。
逗号分隔的using声明允许你在泛型代码中从可变数量的所有基类中派生同一种运算。
这项技术的一个很酷的应用是创建一个重载的lambda的集合。通过如下定义:
// 继承所有基类里的函数调用运算符
template<typename... Ts>
struct overload : Ts...
{
using Ts::operator()...;
};
// 基类的类型从传入的参数中推导
template<typename... Ts>
overload(Ts...) -> overload<Ts...>;
你可以像下面这样重载两个lambda:
auto twice = overload {
[](std::string& s) { s += s; },
[](auto& v) { v *= 2; }
};
这里,我们创建了一个overload
类型的对象,并且提供了推导指引
来根据lambda的类型推导出overload
的基类的类型。
还使用了聚合体初始化
来调用每个lambda生成的闭包类型的拷贝构造函数来初始化基类子对象。
上例中的using声明使得overload
类型可以同时访问所有子类中的函数调用运算符。
如果没有这个using声明,两个基类会产生同一个成员函数operator()
的重载,
这将会导致歧义。
最后,如果你传递一个字符串参数将会调用第一个重载,其他类型(操作符*=
有效的类型)
将会调用第二个重载:
int i = 42;
twice(i);
std::cout << "i: " << i << '\n'; // 打印出:84
std::string s = "hi";
twice(s);
std::cout << "s: " << s << '\n'; // 打印出:hihi
这项技术的另一个应用是std::variant
访问器。
除了逐个声明继承构造函数之外,现在还支持如下的方式:
你可以声明一个可变参数类模板Multi
,让它继承每一个参数类型的基类:
template<typename T>
class Base {
T value{};
public:
Base() {
...
}
Base(T v) : value{v} {
...
}
...
};
template<typename... Types>
class Multi : private Base<Types>...
{
public:
// 继承所有构造函数:
using Base<Types>::Base...;
...
};
有了所有基类构造函数的using声明,你可以继承每个类型对应的构造函数。
现在,当使用不同类型声明Multi<>
时:
using MultiISB = Multi<int, std::string, bool>;
你可以使用每一个相应的构造函数来声明对象:
MultiISB m1 = 42;
MultiISB m2 = std::string("hello");
MultiISB m3 = true;
根据新的语言规则,每一个初始化会调用匹配基类的相应构造函数和所有其他基类的默认构造函数。因此:
MultiISB m2 = std::string("hello");
会调用Base<int>
的默认构造函数、Base<std::string>
的字符串构造函数、
Base<bool>
的默认构造函数。
理论上讲,你也可以通过如下声明来支持Multi<>
进行赋值操作:
template<typename... Types>
class Multi : private Base<Types>...
{
...
// 派生所有赋值运算符
using Base<Types>::operator=...;
};