std::visit 的 C++17 示例中令人困惑的模板

Confusing templates in C++17 example of std::visit

在 cppreference 中查看 std::visit() 页面时, https://en.cppreference.com/w/cpp/utility/variant/visit,我遇到了看不懂的代码...

这是简化版:

#include <iomanip>
#include <iostream>
#include <string>
#include <type_traits>
#include <variant>
#include <vector>

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

int main() {
    std::vector<std::variant<int,long,double,std::string>> vec = { 10, 15l, 1.5, "hello" };
    for (auto& v : vec) {
        std::visit(overloaded{
            [](auto arg) { std::cout << arg << ' '; },
            [](double arg) { std::cout << std::fixed << arg << ' '; },
            [](const std::string& arg) { std::cout << std::quoted(arg) << ' '; },
            }, v);
    }
}

int main() 上方声明 overloaded 的两行是什么意思?

感谢您的解释!

2019年加法
在下面的两位先生提供了详细的解释之后(非常感谢!),我在非常好的书中偶然发现了相同的代码 C++17 in Detail - 了解新 C++ 标准令人兴奋的特性!,作者 Bartłomiej Filipek。写得真好!

What are the two lines declaring overloaded, just above int main(), mean?

第一个

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

是经典class/structdeclaration/definition/implementation。从 C++11 开始有效(因为使用可变参数模板)。

在这种情况下,overloaded 继承自所有模板参数并启用(using 行)所有继承的 operator()。这是 Variadic CRTP.

的示例

不幸的是,可变参数 using 仅从 C++17 开始可用。

第二个

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

是一个 "deduction guide"(有关更多详细信息,请参阅 this page)并且它是一个新的 C++17 功能。

在你的情况下,演绎指南说当你写一些东西时

auto ov = overloaded{ arg1, arg2, arg3, arg4 };

overloaded ov{ arg1, args, arg3, arg4 };

ov 变成 overloaded<decltype(arg1), decltype(arg2), decltype(arg3), decltype(arg4)>

这允许你写一些东西

overloaded
{
    [](auto arg) { std::cout << arg << ' '; },
    [](double arg) { std::cout << std::fixed << arg << ' '; },
    [](const std::string& arg) { std::cout << std::quoted(arg) << ' '; },
}

在 C++14 中是

auto l1 = [](auto arg) { std::cout << arg << ' '; };
auto l2 = [](double arg) { std::cout << std::fixed << arg << ' '; };
auto l3 = [](const std::string& arg) { std::cout << std::quoted(arg) << ' '; }

overloaded<decltype(l1), decltype(l2), decltype(l3)> ov{l1, l2, l3};

-- 编辑 --

正如 Nemo(谢谢!)在您问题的示例代码中指出的那样,还有另一个有趣的新 C++17 功能:基 类.

的聚合初始化

我的意思是...当你写

overloaded
{
    [](auto arg) { std::cout << arg << ' '; },
    [](double arg) { std::cout << std::fixed << arg << ' '; },
    [](const std::string& arg) { std::cout << std::quoted(arg) << ' '; }
 }

您正在传递三个 lambda 函数来初始化 overloaded 的三个基数 类。

在 C++17 之前,只有编写显式构造函数才能执行此操作。从C++17开始,自动运行。

此时,在我看来,用 C++17 显示 overloaded 的简化完整示例和相应的 C++14 示例可能很有用。

我提出以下 C++17 程序

#include <iostream>

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

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

int main ()
{
    overloaded ov
    {
        [](auto arg) { std::cout << "generic: " << arg << std::endl; },
        [](double arg) { std::cout << "double: " << arg << std::endl; },
        [](long arg) { std::cout << "long: " << arg << std::endl; }
    };
    ov(2.1);
    ov(3l);
    ov("foo");      
 }

和我能想象到的最好的 C++14 替代方案(也遵循 bolov 关于 "make" 函数的建议和他的递归 overloaded 示例)。

#include <iostream>

template <typename ...>
struct overloaded;

template <typename T0>
struct overloaded<T0> : public T0
{
    template <typename U0>
    overloaded (U0 && u0) : T0 { std::forward<U0>(u0) }
    { }
};

template <typename T0, typename ... Ts>
struct overloaded<T0, Ts...> : public T0, public overloaded<Ts ...>
{
    using T0::operator();
    using overloaded<Ts...>::operator();

    template <typename U0, typename ... Us>
    overloaded (U0 && u0, Us && ... us)
      : T0{std::forward<U0>(u0)}, overloaded<Ts...> { std::forward<Us>(us)... }
    { }
 };

template <typename ... Ts>
auto makeOverloaded (Ts && ... ts)
{
    return overloaded<Ts...>{std::forward<Ts>(ts)...};
}

int main ()
{
    auto  ov
    {
        makeOverloaded
        (
            [](auto arg) { std::cout << "generic: " << arg << std::endl; },
            [](double arg) { std::cout << "double: " << arg << std::endl; },
            [](long arg) { std::cout << "long: " << arg << std::endl; }
        )
    };
    ov(2.1);
    ov(3l);
    ov("foo");      
 }

我想这是一个见仁见智的问题,但在我看来,C++17 版本更简单、更优雅。

啊,我喜欢这个。

这是一种使用在模板参数调用运算符集上重载的调用运算符来简洁地声明结构的方法。

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

overloaded 继承自 Ts... 并使用它们的所有 operator()

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

这是推导指南,因此您无需指定模板参数

用法如示例所示。

创建一组重载的多个 lambda(和其他函数类型)是一个很好的实用程序。


在 C++17 之前,您必须使用递归来创建 overload。不漂亮:

template <class... Fs> struct Overload : Fs...
{
};

template <class Head, class... Tail>
struct Overload<Head, Tail...> : Head, Overload<Tail...>
{
    Overload(Head head, Tail... tail)
        : Head{head}, Overload<Tail...>{tail...}
    {}

    using Head::operator();
    using Overload<Tail...>::operator();
};


template <class F> struct Overload<F> : F
{
    Overload(F f) : F{f} {}

    using F::operator();
};


template <class... Fs> auto make_overload_set(Fs... fs)
{
    return Overload<Fs...>{fs...};
}

auto test()
{
    auto o = make_overload_set(
         [] (int) { return 24; },
         [] (char) { return 11; });

    o(2); // returns 24
    o('a'); // return 11
}

主要的麻烦是 Overload 因为继承不是聚合,所以您需要使用递归技巧来创建具有所有类型的构造函数。在 C++17 中,overloaded 是一个集合(是的),因此构造一个开箱即用的集合:)。您还需要为它们中的每一个指定 using::operator()