鹿角站 - Misdeer's Antlers

MisDeer's Antlers
天下人共逐之
  1. 首页
  2. C++
  3. 正文

【C++黑魔法】重载模式

2022-07-30 199点热度 0人点赞 0条评论

C++17 引入了类似union的std::variant,相比union其最大好处是它保存了类型,可以在运行期获取当前所持有的类型。
同时C++17 提供了std::visit,用来将一系列访问器函数打包到一起,根据 std::variant 中的类型进行动态地处理。

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

using var_t = std::variant<int, long, double, std::string>;
struct VisitPack {
    void operator()(auto arg) { std::cout << "auto: " << arg << std::endl; }
    void operator()(double arg) { std::cout << "double: " << arg << std::endl; }
    void operator()(const std::string& arg) { std::cout << "string: " << arg << std::endl; }
};

int main() {
    std::vector<var_t> vec = {10, 15l, 1.5, "hello"};
    for (auto& v: vec) {
        std::visit(VisitPack(), v);
    }
    return 0;
}

输出

auto: 10
auto: 15
double: 1.5
string: hello

为了更简洁地使用std::visit,我们可以使用重载模式(Overload pattern)
cppreference上的std::visit的页面有一个关于重载模式很好的样例。
https://en.cppreference.com/w/cpp/utility/variant/visit
下面是简化后的代码

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

using var_t = std::variant<int, long, double, std::string>;
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

int main() {
    std::vector<var_t> vec = {10, 15l, 1.5, "hello"};
    for (auto& v: vec) {
        std::visit(overloaded {
            [](auto arg) { std::cout << "auto: " << arg << std::endl; },
            [](double arg) { std::cout << "double: " << arg << std::endl; },
            [](const std::string& arg) { std::cout << "string: " << arg << std::endl; }
        }, v);
    }
    return 0;
}

解释下其中两行关键代码

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

第一行使用的黑魔法叫做 变参模板(Template parameter pack, since C++11) 和 using语句解包(since C++17)
https://en.cppreference.com/w/cpp/language/parameter_pack
变参模板使得你可以在定义模板时使用不定长的模板参数(类似于变参函数)
这里定义了模板类 overloaded,模板参数为参数包class... Ts,并且这个类继承自参数包中的模板参数。
在类的实现中使用using语句解包,来将继承的父类(即后续传入的一系列lambda表达式)中的 operator()声明引入到overloaded类的空间中。

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
\\ 传入三个模板参数 T0, T1, T2 后,等价与以下代码
template<T0, T1, T2>
struct overloaded : T0, T1, T2 {
    using T0::operator();
    using T1::operator();
    using T2::operator();
}

第二行使用的黑魔法叫做自定义类模板推导(Class template argument deduction, CTAD, since C++17)
https://en.cppreference.com/w/cpp/language/class_template_argument_deduction
模板推导,即当我们在使用模板时,编译器在编译期可以自动根据初始化参数来推断合适的模板参数。这个特性在声明一个拥有复杂类型的模板时,还是比较方便的。
举个例子,比如我们需要初始化一个std::vector<int>,我们可以这么写

std::vector<int> v {1, 2, 3, 4}; \\ 显式声明模板参数
std::vector v {1, 2, 3, 4}; \\ 省略模板参数,编译器自动推导为 int

因为编译器在编译期,为每个lambda表达式生成了一个匿名的类型,所以我们没有办法提前知道其类型,因此也没法确定模板参数是哪些。
在C++17以前,我们需要写一个make_xxx模板函数来辅助构造,就像std::make_pair那样

template<class... Ts>
constexpr auto make_overloaded(Ts&&... t){
    \\ 传入若干个lambda表达式的实例
    \\ 返回一个overloaded类,初始化参数是lambda表达式实例的右值引用,模板参数是lambda表达式的类型
    return overloaded<Ts...>{std::forward<Ts>(t)...};
}
auto my_overloaded = make_overloaded(...); \\ 使用make_overloaded函数来构造overloaded类

C++17开始,可以通过自定义类模板推导的规则,告诉编译器初始化参数和模板参数的关系,实现类似的功能。

\\ 模板参数推导时,告诉编译器,把初始化参数的当做模板参数
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
\\ 直接初始化构造overloaded类,构造时省略模板参数
overloaded my_overloaded {...};

最后使用时,构造了一个匿名的overloaded类,来作为std::visit的访问器。

std::visit(overloaded {
    [](auto arg) { std::cout << "auto: " << arg << std::endl; },
    [](double arg) { std::cout << "double: " << arg << std::endl; },
    [](const std::string& arg) { std::cout << "string: " << std::quoted(arg) << std::endl; }
}, v);
标签: 暂无
最后更新:2022-07-30

misdeer

桑之未落,其叶沃若。

点赞

文章评论

您需要 登录 之后才可以评论

COPYRIGHT © 2022 MisDeer's Antlers. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang

闽ICP备2022011002号