"operator()..."语法在C++中是什么意思?

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

本文关键字:是什么 意思 C++ operator 语法      更新时间:2023-10-16

我试图理解cpp首选项中的std::visit示例,我在其中看到了以下代码行:

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

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

我想在这里补充一些历史课。

这里有很多层,所以让我们一个接一个地剥开它们。

  • 可变参数模板 (C++11)
  • 参数包
  • 包扩展
  • using宣言》
  • 用于引入基类成员
  • 可变参数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){}。这被称为包扩展,同样,这不是一个令人惊讶的名字。

函数调用的包扩展结果如下:

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

可以这样想:

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

其中Arguments是一种异构数组(intdoublechar)。

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

template<class... Ts> struct overloaded

声明一个类overloaded,该类可以在"无限"数量的类型上进行模板化。

它的: Ts...部分:

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

使用 pack 扩展来声明要从每种类型派生(可能通过多重继承)的类overloaded


using声明

在 C++11 之前,我们可以声明带有如下typedef的类型别名:

typedef unsigned int uint;

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

using uint = unsigned int;

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

没有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语句有一个小差距没有得到解决; "如果我想为每个基类提供一个using,其中这些基类是模板参数怎么办?">

所以像这样:

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 出现了,给了我们使用支持的变化,以便我们可以做到这一点:

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的类,它在"无限"数量的类型上模板化。它派生自这些类型中的每一种。它还允许您使用每种父类型的operator()方法。方便,对吧?(请注意,如果任何基类operator()看起来相同,我们就会得到一个错误。

>模板演绎指南 困扰C++开发人员一段时间的另一件事是,如果你有一个模板化类,并且还有一个模板化构造函数,你必须显式指定模板参数,即使你认为模板类型对你自己和你的客户端来说很明显应该是什么。

例如,我将要编写一个轻量级迭代器包装器:

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引入了演绎指南,我们作为类编写者可以做一些额外的工作,这样客户就不必这样做了。现在我作为类作者可以写这个:

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...>;

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

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

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

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

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

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

using Foo::operator();
using Bar::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();
};