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);
文章评论