最近研究有关C ++ 17的书和博客文章时,偶然发现了这种访问模式std::variant。使用overload模式,您可以“访问”提供单独的lambda。
#include
#include
using namespace std;
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
int main()
{
overloaded s{
[](int){cout << "int" << endl;},
[](double){cout << "double" << endl;},
[](string){cout << "string" << endl;},
};
s(1); // int
s(1.0); // double
s("1"); // string
}
template struct overloaded : Ts... { using Ts::operator()...; };
template
:overloaded 类的模板参数为可变长的参数包 Ts。T1, T2, … , TN
,那么这一句声明可以展开为:template
struct overloaded : Ts...
:overloaded 类的基类为参数包 Ts 内所有的参数类型。using Ts::operator()...;
:这是一个变长 using 声明。using T1::operator(), T1::operator(), ..., TN::operator();
template overloaded(Ts...) -> overloaded;
a1, a2, …, an
的类型分别为T1, T2, …, TN
,overloaded {a1, a2, ..., an}
的类型就是 overloaded
。*overloaded s{
[](int){cout << "int" << endl;},
[](double){cout << "double" << endl;},
[](string){cout << "string" << endl;},
};
overloaded
,其中T1, T2, T3为3个lambda参数的类型。本文顶部提到的代码形成了一个称为overload(或有时为overloaded)的模式,它对std::variant访问最为有用。
使用此类帮助程序代码,您可以编写:
template<class... Ts> struct overload : Ts... { using Ts::operator()...; };
template<class... Ts> overload(Ts...) -> overload<Ts...>;
int main()
{
std::variant<int, float, std::string> intFloatString { "Hello" };
std::visit(
overloaded{
[](const int& i) { std::cout << "int: " << i; },
[](const float& f) { std::cout << "float: " << f; },
[](const std::string& s) { std::cout << "string: " << s; }
},
intFloatString
);
}
如果没有 overload
,则必须为调用operator ()
写一个具有三个重载的单独的类或结构:
struct PrintVisitor
{
void operator()(int& i) const {
std::cout << "int: " << i;
}
void operator()(float& f) const {
std::cout << "float: " << f;
}
void operator()(std::string& s) const {
std::cout << "string: " << s;
}
};
int main()
{
std::variant<int, float, std::string> intFloatString { "Hello" };
std::visit(PrintVisitor(), intFloatString);
}
那么overload模式如何工作?为什么我们需要从那里的lambda继承?
您可能已经知道编译器在概念上将lambda表达式扩展为具有operator()的唯一命名类型。我们在overload模式中所做的是,我们继承了几个lambda,然后为std::visit
公开了它们的operator()
。这样,您可以“就地”编写重载。
什么是组成C++17特征的重要拼图?
让我们逐节探讨组成overload模式的新元素。这样,我们就可以学习有关该语言的一些有趣的东西。
如您所见,我们有三种描述的功能,很难说出哪一种是最简单的解释。但是,让我们从Using开始。为什么我们必须需要它?
为了理解这一点,让我们编写一个从两个基类派生的简单类型:
#include
struct BaseInt {
void Func(int) { std::cout << "BaseInt...\n"; }
};
struct BaseDouble {
void Func(double) { std::cout << "BaseDouble...\n"; }
};
struct Derived : public BaseInt, BaseDouble {
//using BaseInt::Func;
//using BaseDouble::Func;
};
int main() {
Derived d;
d.Func(10.0);
}
我们有两个实现Func的基类。我们想从派生对象中调用该方法。 代码会编译吗?NO!
在进行overload解决方案设置时,C ++指出最佳可行功能必须在同一范围内。因此,GCC报告了以下错误:
这就是为什么我们必须将函数纳入派生类的范围。我们已经解决了一部分,它不是C ++ 17的功能。但是可变参数语法呢?这里的问题是在C ++ 17之前不支持使用…
在文章Pack expansions in using-declarations P0195R2 ,有一个激励性的示例显示了需要多少额外的代码来减轻该限制:
template <typename T, typename... Ts>
struct Overloader : T, Overloader<Ts...> {
using T::operator();
using Overloader<Ts...>::operator();
// […]
};
template <typename T> struct Overloader<T> : T {
using T::operator();
};
在上面的示例中,在C ++ 14中,我们必须创建一个递归模板定义才能使用using。但是现在我们可以写:
template <typename... Ts>
struct Overloader : Ts... {
using Ts::operator()...;
// […]
};
现在要简单得多! 好的,但是其余的代码呢?
我们从lambda派生,然后如上一节所述,公开它们的operator()。但是我们如何创建这种重载类型的对象呢?
如您所知,无法预先了解lambda的类型,因为编译器必须为它们每个生成一些唯一的类型名称。例如,我们不能只写:
overload<LambdaType1, LambdaType2> myOverload { ... } // ???
// what is LambdaType1 and LambdaType2 ??
唯一可行的方法是make函数(因为模板参数推导适用于函数模板,因为一如既往):
template <typename... T>
constexpr auto make_overloader(T&&... t) {
return Overloader<T...>{std::forward<T>(t)...};
}
使用C ++ 17中添加的模板参数推导规则,我们可以简化常见模板类型的创建,并且不需要make_overloader
函数。
例如,对于简单类型,我们可以编写:
std::pair strDouble { std::string{"Hello"}, 10.0 };
// strDouble is std::pair
您还可以选择定义自定义推论指南。标准库使用了很多它们,例如,用于std::array
:
template <class T, class... U>
array(T, U...) -> array<T, 1 + sizeof...(U)>;
以上规则使我们可以编写:
array test{1, 2, 3, 4, 5};
// test is std::array
对于overload模式,我们可以这样写:
template<class... Ts> overload(Ts...) -> overload<Ts...>;
现在,我们可以输入
overload myOverload { [](int) { }, [](double) { } };
并且可以正确推断出用于overload 的模板参数。在我们的例子中,编译器知道lambda将是什么类型
现在,让我们转到难题的最后一个缺少的部分-聚合初始化。
此功能相对简单:我们现在可以初始化从其他类型派生的类型。
提醒一句: from dcl.init.aggr:
集合是具有以下内容的数组或类:
*没有用户提供的,显式或继承的构造函数
*没有私有或受保护的非静态数据成员
*没有虚函数
*没有virtual, private, protected的基类
For example (sample from the spec draft):
struct base1 { int b1, b2 = 42; };
struct base2 {
base2() { b3 = 42; }
int b3;
};
struct derived : base1, base2 {
int d;
};
derived d1{{1, 2}, {}, 4};
derived d2{{}, {}, 4};
初始化 d1.b1 with 1, d1.b2 with 2, d1.b3 with 42, d1.d with 4, and d2.b1 with 0, d2.b2 with 42, d2.b3 with 42, d2.d with 4.
就我们而言,它具有更大的影响。因为对于重载类,没有聚合初始化,我们必须实现以下构造函数:
struct overload : Fs...
{
template <class ...Ts>
overload(Ts&& ...ts) : Fs{std::forward<Ts>(ts)}...
{}
// ...
}
这需要编写很多代码,并且可能没有涵盖noexcept之类的所有情况 。使用聚合初始化时,我们从基类列表中“直接”调用了lambda的构造函数,因此无需编写它并直接向其转发参数。
对于每个C ++版本,通常都有机会编写更紧凑的代码。使用C ++ 20,语法可能会更短。
在C ++ 20中,对类模板参数推导进行了扩展,并且自动处理了聚合。这意味着您无需编写自定义扣除指南。
举个简单例子
template <typename T, typename U, typename V>
struct Triple { T t; U u; V v; };
在c++20你可以写
Triple ttt{ 10.0f, 90, std::string{"hello"}};
T将推导为float
,U将推导为int
,V将推导为std :: string
。 C ++ 20中的重载模式现在只是:
template<class... Ts> struct overload : Ts... { using Ts::operator()...; };
有关此功能的建议,请参见c++标准P1021和P1816。 GCC10似乎实施了此建议,但不适用于具有继承功能的高级案例,因此我们必须在这里等待完全的遵循。
The overload pattern是一件令人着迷的事情。它演示了几种C ++技术,将它们收集在一起,并允许我们编写较短的语法。
在C ++ 14中,您可以从lambda派生并构建类似的帮助程序类型,但是只有使用C ++ 17,您才能显着减少样板代码并限制潜在的错误。使用C ++ 20时,由于CTAD可与聚合一起使用,因此语法将更短。
您可以在有关overload P0051的建议中阅读更多信息(C ++ 20不接受此建议,但是值得一看,以了解其背后的讨论和概念)。
本博客文章中介绍的模式仅支持lambda,并且没有处理常规函数指针的选项。在本文中,您可以看到尝试处理所有情况的更高级的实现。
参考文章:
More To read & References
You might also like to read: