The overload Pattern

最近研究有关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。
    假设 Ts 包含 T1, T2, … , TN,那么这一句声明可以展开为:template
  • struct overloaded : Ts...:overloaded 类的基类为参数包 Ts 内所有的参数类型。
    假设 Ts 包含 T1, T2, … , TN,那么这一句声明可以展开为:struct overloaded : T1, T2, …, TN
  • using Ts::operator()...;:这是一个变长 using 声明。
    假设 Ts 包含 T1, T2, … , TN,那么这一句声明可以展开为:using T1::operator(), T1::operator(), ..., TN::operator();
    也就是说,overloaded 类的基类即参数包 Ts 内所有的参数类型的函数调用操作符均被 overloaded 类引入了自己的作用域。
  • template overloaded(Ts...) -> overloaded;
    这是一个自动推断向导,用于帮助编译器根据 overloaded 构造器参数的类型来推导 overloaded 的模板参数类型。

    这个自动推断向导告诉编译器,如果 overloaded 构造器所有参数的类型的集合为Ts,那么 overloaded 的模板参数类型就是 Ts 所包含的所有类型。

    也就是说如果表达式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 类的实例 s 的构造器包含3个lambda参数,也可以看作3个各自包含一个 operator() 的函数对象。

    根据 overloaded 类的定义,s 对象将继承这3个lambda(函数对象)的 operator() ,也就是说这3个lambda的 operator() 即函数体在 s 对象内部形成重载关系。

    根据 overloaded 类的自动推断向导,s 对象的类型为overloaded,其中T1, T2, T3为3个lambda参数的类型。
  • 通过利用 C++17 的新特性变长的 using 声明以及自动推断向导,overloaded类的实例可以简洁并且巧妙地将多个lambda合成一个大的具有多个相互重载的 operator() 的函数对象。
  • overloaded 这个类模板如此有用,实现机制又如此精妙,实在是应该早日纳入标准库中。

介绍

本文顶部提到的代码形成了一个称为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
	);
}

The overload Pattern_第1张图片
如果没有 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特征的重要拼图?

  • 使用声明进行打包扩展(Pack expansions in using declarations)- 可变参数模板的简短语法。
  • 自定义模板参数推导规则(Custom template argument deduction rules) -允许将lambda对象列表转换为重载类的基类列表。 (注意:C ++ 20中不需要!)
  • 扩展聚合初始化(Extension to aggregate Initialization)- 在C ++ 17之前,您不能聚合从其他类型派生的初始化类型。

C++17的新特性

让我们逐节探讨组成overload模式的新元素。这样,我们就可以学习有关该语言的一些有趣的东西。

Using Declarations

如您所见,我们有三种描述的功能,很难说出哪一种是最简单的解释。但是,让我们从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()...;
    // […]
};

现在要简单得多! 好的,但是其余的代码呢?

Custom Template Argument Deduction Rules

我们从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将是什么类型

现在,让我们转到难题的最后一个缺少的部分-聚合初始化。

Extension to Aggregate Initialisation

此功能相对简单:我们现在可以初始化从其他类型派生的类型。

提醒一句: 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++20 Updates

对于每个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似乎实施了此建议,但不适用于具有继承功能的高级案例,因此我们必须在这里等待完全的遵循。

Summary

The overload pattern是一件令人着迷的事情。它演示了几种C ++技术,将它们收集在一起,并允许我们编写较短的语法。

在C ++ 14中,您可以从lambda派生并构建类似的帮助程序类型,但是只有使用C ++ 17,您才能显着减少样板代码并限制潜在的错误。使用C ++ 20时,由于CTAD可与聚合一起使用,因此语法将更短。

您可以在有关overload P0051的建议中阅读更多信息(C ++ 20不接受此建议,但是值得一看,以了解其背后的讨论和概念)。

本博客文章中介绍的模式仅支持lambda,并且没有处理常规函数指针的选项。在本文中,您可以看到尝试处理所有情况的更高级的实现。


参考文章:

  • Bartlomiej Filipek - 2 Lines Of Code and 3 C++17 Features - The overload Pattern
  • zwvista -csdn - C++17尝鲜:变长 using 声明

More To read & References

  • aggregate initialization - cppreference.com
  • Everything You Need to Know About std::variant from C++17
  • How To Use std::visit With Multiple Variants
  • C++ Weekly - Ep 49 - Why Inherit From Lambdas?
  • C++ Weekly - Ep 48 - C++17’s Variadic using
  • C++ Weekly - Ep 40 - Inheriting From Lambdas
  • Overload: Build a Variant Visitor on the Fly - Simplify C++!

You might also like to read:

  • Activity Indicators - Example of a Modern C++ Library
  • Runtime Polymorphism with std::variant and std::visit
  • Notes on C++ SFINAE, Modern C++ and C++20 Concepts
  • “Use the Force, Luke”… or Modern C++ Tools
  • In-class Member Initialisation: From C++11 to C++20

你可能感兴趣的:(C/C++)