"operator()..." 语法在 C++ 中意味着什么?

What does the "operator()..." syntax mean in C++?

我试图从 cppreference 中理解 std::visit 的示例,在那里我看到了以下代码行:

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

我不明白。 operator()... 在代码中是什么意思?

这里的语法是<tokens>....

在您的特定情况下,以下是为一个、两个和三个参数扩展重载结构的方式:

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };

一个参数:

template <class A> struct overloaded : A { using A::operator(); };

两个参数:

template<typename A, typename B>
struct overloaded: A, B
{
    using A::operator(); using B::operator();
};

三个参数:

template<typename A, typename B, typename C>
struct overloaded: A, B, C
{
    using A::operator(); using B::operator(); using C::operator();
};

要理解using Ts::operator()...;,首先你必须知道class... Ts是一个参数包(可变模板的)。它是一系列 0 ... N 模板类型参数。

using Ts::operator()... 中的省略号是参数包 扩展 的语法。例如,在 overloaded<Foo, Bar> 的情况下, using Ts::operator()...; 声明将扩展为等效于:

using Foo::operator();
using Bar::operator();

我想通过一些历史课来补充这里的精彩答案。

这里有很多层次,让我们一层一层地揭开它们。

  • 可变参数模板 (C++11)
  • 参数包
  • 包扩展
  • using 声明
  • 用于介绍基地 class 成员
  • 可变参数 using 声明 (C++17)
  • 模板推导指南 (C++17)

可变参数模板

在 C++11 之前,我们在函数可以接收的模板参数数量上受到程序员愿意输入的数量的限制。

例如,如果我想编写一个函数来对可能不同类型的“任意”数量的值求和,我需要编写大量样板文件,即使那样我也受到限制:

template<class T>
void foo(T){}

template<class T, class U>
void foo(T, U){}

template<class T, class U, class V>
void foo(T, U, V){}

// ... and so on until I decide enough's enough

在 C++11 中,我们终于收到了“可变参数模板”,这意味着我们可以通过使用省略号 (...) 接收“无限”(由您的编译器确定的实际限制)数量的模板参数,所以现在我们可以写

template<class... T>
void foo(T... args){}

这个“无限数量”的模板参数 class... T 被称为“参数包”,因为它毫无疑问地代表了一组参数。

为了将这些参数“解压缩”到逗号分隔的列表中,我们在函数参数列表中再次使用省略号:void foo(T... args){}。这被称为 pack expansion,再一次,这不是一个令人惊讶的名字。

像这样的函数调用的包扩展结果:

int a = /*...*/;
double b = /*...*/;
char c = /*...*/;
foo(a, b, c);

可以这样想:

template<int, double, char>
void foo(Arguments[3] args){}

其中Arguments是(int,double,char)的一种异构数组。

这些可变参数模板也适用于 classstruct 模板,所以这里的模拟是

template<class... Ts> struct overloaded

声明一个 class overloaded 可以在“无限”数量的类型上进行模板化。

它的 : Ts... 部分:

template<class... Ts> struct overloaded : Ts...

使用包扩展来声明 class overloaded 从每个类型派生(可能通过多重继承)。


using声明

在 C++11 之前,我们可以像这样用 typedef 声明类型别名:

typedef unsigned int uint;

在 C++11 中,我们收到了可以做同样事情的 using 语句,也许更清楚一点(还有更多!等一下)

using uint = unsigned int;

然而,using 语句最初用于不同的用途(自从引入 C++11 以来,它的用途已大大扩展)。创建它的主要原因之一是我们可以在派生 classes 中重用基础 classes 中的东西,而无需强制客户端消除歧义:

没有using

struct does_a_thing
{
    void do_a_thing(double){}
};

struct also_does_a_thing
{
    void do_a_thing(int){}
};

struct derived : does_a_thing, also_does_a_thing{};

int main(){
    derived d;
    d.do_a_thing(1); // ? which "do_a_thing gets called? Neither, because it's ambiguous, so there's a compiler error
    d.does_a_thing::do_a_thing(42.0);
    d.also_does_a_thing::do_a_thing(1);
    
}

请注意,客户端被迫编写一些时髦的语法来引用他们想要用于调用 do_a_thingderived 的哪个基数。如果我们利用 using:

这看起来会更好

using:

struct derived : does_a_thing, also_does_a_thing
{
    using does_a_thing::do_a_thing;
    using also_does_a_thing::do_a_thing;
};

int main(){
    derived d;
    d.do_a_thing(1); // calls also_does_a_thing::do_a_thing
}

更干净,对吧?


可变参数 using 声明

所以 C++11 出来了,我们都对这些新特性印象深刻,但是 using 语句有一个小缺口没有解决; “如果我想为每个基数 class 设置一个 using,这些基数 class 是模板参数怎么办?”

所以像这样:

template<class T, class U>
struct derived : T, U
{
    using T::do_a_thing;
    using U::do_a_thing;
};

int main(){
    derived<does_a_thing, also_does_a_thing> d;
    d.do_a_thing(1); // calls also_does_a_thing::do_a_thing
}

到目前为止,还不错。但是既然我们了解了 可变参数模板 ,让我们把 derived 变成一个:

template<class... Ts>
struct derived : Ts...
{
   //using ?
};

当时,using 由于缺乏可变参数支持而受到阻碍,因此我们无法(轻松)做到这一点。

然后 C++17 出现并给了我们 variadic using 支持,所以我们可以做到:

template<class... Ts>
struct derived : Ts...
{
   using Ts::do_a_thing...;
};

int main(){
    derived<does_a_thing, also_does_a_thing> d;
    d.do_a_thing(1); // calls also_does_a_thing::do_a_thing
    d.do_a_thing(42.0); //calls does_a_thing::do_a_thing
}

我们终于可以理解您的代码的第一部分了!

所以现在我们终于可以理解这部分问题的全部了:

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...;};

我们有一个名为 overloaded 的 class,它以“无限”数量的类型为模板。它源自这些类型中的每一种。它还允许您使用每个父类型的 operator() 方法。方便吧? (请注意,如果任何基数 class' operator() 看起来相同,我们就会得到一个错误。)


模板推导指南

另一件困扰 C++ 开发人员一段时间的事情是,如果你有一个模板化的 class,它也有一个模板化的构造函数,你必须显式指定模板参数,即使你认为这是显而易见的你自己和你的客户模板类型应该是什么。

例如,我想写一个轻量级的迭代器包装器:

template<class T>
struct IteratorWrapper
{
    template<template<class...> class Container, class... Args>
    IteratorWrapper(const Container<Args...>& c)
    {
        // do something with an iterator on c
        T begin = c.begin();
        T end = c.end();
        while(begin != end)
        {
            std::cout << *begin++ << " ";
        } 
    } 
};

现在,如果我作为调用者想要创建 IteratorWrapper 的实例,我必须做一些额外的工作来消除 T 到底是什么的歧义,因为它不包含在构造函数的签名中:

std::vector<int> vec{1, 2, 3};
IteratorWrapper<typename std::vector<int>::const_iterator> iter_wrapper(vec);

没有人愿意写那个怪物。因此,C++17 引入了演绎指南,我们作为 class 编写者可以做一些额外的工作,这样客户就不必这样做了。现在我作为 class 作者可以这样写:

template<template<class...> class Container, class... Args>
IteratorWrapper(const Container<Args...>& c) -> IteratorWrapper<typename Container<Args...>::const_iterator>;

它模仿 IteratorWrappers 构造函数的签名,然后使用尾部箭头 (->) 指示要推导的 ItearatorWrapper 的类型。

所以现在我的客户可以这样写代码了:

std::vector<int> vec{1, 2, 3};
IteratorWrapper iter_wrapper(vec);

std::list<double> lst{4.1, 5.2};
IteratorWrapper lst_wrapper(lst);

很漂亮吧?


我们现在可以看懂第二行代码了

template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

为我们的 class overloaded 声明一个 模板推导指南 说当使用参数包调用其构造函数时, class 也应该在这些相同的类型上进行模板化。

这听起来可能没有必要,但如果您有一个带有模板构造函数的模板 class,您可能需要它:

template<class... T>
struct templated_ctor{
    template<class... U>
     overloaded(U...){}
};

* 我知道我在这里过火了,但是写下来并真正彻底地回答问题很有趣:-)