c++ - `operator()...`在C++代码中是什么意思?

标签 c++ templates operator-keyword c++17 variant

我试图理解 std::visit 的例子来自 cppreference ,在那里我看到了以下代码行:

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是一种 ( int , double , char ) 的异构数组。
    这些可变参数模板也适用于 classstruct模板,所以这里的模拟是
    template<class... Ts> struct overloaded
    
    声明一个类 overloaded可以在“无限”数量的类型上进行模板化。: Ts...其中的一部分:
    template<class... Ts> struct overloaded : Ts...
    
    使用包扩展来声明类 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);
        
    }
    
    请注意,客户端被迫编写一些时髦的语法来引用 derived 的基数。他们想用于拨打 do_a_thing .如果我们利用 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还有一个小差距。 Unresolved 陈述; “如果我想为每个基类设置一个 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 出现并为我们提供了可变参数 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 的类以“无限”数量的类型为模板。它派生自这些类型中的每一种。并且它还允许您使用 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 引入了演绎指南,我们作为类(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也就是说,当使用参数包调用其构造函数时,该类也应该在这些相同类型上进行模板化。
    这听起来可能没有必要,但如果您有一个带有模板化构造函数的模板化类,您可能会想要它:
    template<class... T>
    struct templated_ctor{
        template<class... U>
         overloaded(U...){}
    };
    

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

    关于c++ - `operator()...`在C++代码中是什么意思?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46604950/

    相关文章:

    c++ - 用于保存两种类型但一次只能保存一种的模板结构

    haskell - 关于 Haskell 中的 ~ 和 @ 运算符的问题

    c++ - 无法使用 boost enable_if 匹配模板方法

    python - 压缩 if 语句

    c++ - 如何使共享值在没有互斥锁的情况下保持一致?

    c++ - 迭代 std::map<X,std::vector<Y>> 并对 vector 进行排序

    c++ - 附加冒号在模板类中意味着什么。类名<T, SIZE>::类名:

    javascript - 在 underscore.js 模板中使变量可选

    c++ - std::map.insert "could not deduce template argument for..."

    c++ - 清除任意二维数组