一、std::visit的使用
首先看一下std::visit在库中的声明:
c
template <class Visitor, class... Variants>
constexpr visit(Visitor&& vis, Variants&&... vars);
//return
std::invoke(std::forward<Visitor>(vis), std::get<is>(std::forward<Variants>(vars))...)
//, where is... is vars.index()....
如果Variants提供了内部的visit,也可以直接调用其进行处理。这就看具体的内部实现了。
要想明白std::visit是如何实现的,首先要从外部的应用上入手,看看它是如何被应用的,看一下例程:
c
#include <iostream>
int main() {
using typeV = std::variant<int, float, std::string>;
auto func = [](auto &&v) {
using T = std::decay_t<decltype(v)>;
if constexpr (std::is_same_v<T, int>) {
std::cout << "int : " << v << std::endl;
}
if constexpr (std::is_same_v<T, float>) {
std::cout << "float : " << v << std::endl;
}
if constexpr (std::is_same_v<T, std::string>) {
std::cout << "string : " << v << std::endl;
}
};
typeV v{1};
std::visit(func, v);
v = "hello world";
std::visit(func, v);
v = 1.1f;
std::visit(func,v);
return 0;
}
以上面的std::variant为例子,发现std::visit访问需要支持以下的情况:
- 类型擦除
毕竟std::variant本身就是支持不同类型的类似union实现,所以std::visit就必须支持类型擦除,否则无法进行安全正确的访问 - 静态展开和编译时多态
这个在讲编译期和运行时就分析过,要想提高效率就必须支持静态展开和编译时多态(静多态 ) - 效率高且支持编译时优化
静态展开后的应用比动态运行时再判断要效率提高很多。而编译期又可对可预测分支和内联提供支持,保证进一步的优化从而提高效率 - 支持类型恢复和访问函数的调用
既然有类型擦除,又必须有类型恢复,从而能够得到准确的数据,这些数据又可以通过匹配的函数(重载)来进行访问 - 函数模板的实例化
即std::visit通过传入的函数或lambda表达式以重载的形式来进行处理和访问多种不同的数据类型。在这个过程中,编译器会为其生成函数模板的实例。 - 获取访问值和类型
这是std::visit的最终目标,即通过相关的内部机制来获取具体的数据类型,进一步获取访问的值
二、手动实现和测试
下面看一种手动实现的方法:
c
#include <cstddef>
#include <variant>
#include <iostream>
// 基础模板,用于处理访问完成结束,提供一个空实现
template <std::size_t Idx, typename Visitor, typename Variant> struct visit_impl {
static void apply(std::size_t index, Visitor &&, Variant &&) {
}
};
// 递归处理索引为 Idx的类型
template <std::size_t Idx, typename Visitor, typename Variant>
requires(Idx < std::variant_size_v<std::remove_reference_t<Variant>>) struct visit_impl<Idx, Visitor, Variant> {
static void apply(std::size_t id, Visitor &&vis, Variant &&var) {
if (id == Idx) {
std::forward<Visitor>(vis)(std::get<Idx>(std::forward<Variant>(var)));
} else {
// 否则,继续尝试下一个索引(类型)
visit_impl<Idx + 1, Visitor, Variant>::apply(id, std::forward<Visitor>(vis), std::forward<Variant>(var));
}
}
};
// 自定义 visit接口
template <typename Visitor, typename Variant> void customize_visit(Visitor &&vis, Variant &&var) {
std::size_t id = var.index();
// 从索引0遍历
visit_impl<0, Visitor, Variant>::apply(id, std::forward<Visitor>(vis), std::forward<Variant>(var));
}
//别名
using Var = std::variant<int, long, double, std::string>;
// 定义Visitor
struct Visitor {
void operator()(int arg) const { std::cout << "int: " << arg << std::endl; }
void operator()(long arg) const { std::cout << "long: " << arg << std::endl; }
void operator()(double arg) const { std::cout << "double: " << arg << std::endl; }
void operator()(const std::string &arg) const { std::cout << "string: " << arg << std::endl; }
};
int main() {
Var v1 = 6.60;
Var v2 = std::string("Test");
customize_visit(Visitor{}, v1);
customize_visit(Visitor{}, v2);
return 0;
}
这段代码里使用C++20中的requires,如果不想使用可自行将其ID界限和引用移除单独进行处理(比如使用SFINAE)即可。对于这类访问基本都是用ID序列之类的方式来实现的,如果想实现一个更全面和安全的例子,可参考标准库中的std::visit代码实现部分。
三、分析说明
在上面的实现中,其实分成三部分:
- 基础模板visit_impl的实现
通过偏特化和基础模板的处理来实现递归的调用 - visit接口的实现
提供对基础模板等的调用和对外开放访问接口 - 细节的完善
这里面包括对索引范围的限制以及对相关引用限定的处理
看一下编译后的代码:
c
#include <cstddef>
#include <variant>
#include <iostream>
template<std::size_t Idx, typename Visitor, typename Variant>
struct visit_impl
{
static inline void apply(std::size_t index, Visitor &&, Variant &&)
{
}
};
/* First instantiated from: insights.cpp:18 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct visit_impl<1, Visitor, std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &>
{
static inline void apply(std::size_t id, Visitor && vis, std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > & var)
{
if(id == 1UL) {
static_cast<const Visitor &&>(std::forward<Visitor>(vis)).operator()(std::get<1UL>(std::forward<std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &>(var)));
} else {
visit_impl<2, Visitor, std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &>::apply(id, std::forward<Visitor>(vis), std::forward<std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &>(var));
}
}
};
#endif
/* First instantiated from: insights.cpp:18 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct visit_impl<2, Visitor, std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &>
{
static inline void apply(std::size_t id, Visitor && vis, std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > & var)
{
if(id == 2UL) {
static_cast<const Visitor &&>(std::forward<Visitor>(vis)).operator()(std::get<2UL>(std::forward<std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &>(var)));
} else {
visit_impl<3, Visitor, std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &>::apply(id, std::forward<Visitor>(vis), std::forward<std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &>(var));
}
}
};
#endif
/* First instantiated from: insights.cpp:18 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct visit_impl<3, Visitor, std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &>
{
static inline void apply(std::size_t id, Visitor && vis, std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > & var)
{
if(id == 3UL) {
static_cast<const Visitor &&>(std::forward<Visitor>(vis)).operator()(std::get<3UL>(std::forward<std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &>(var)));
} else {
visit_impl<4, Visitor, std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &>::apply(id, std::forward<Visitor>(vis), std::forward<std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &>(var));
}
}
};
#endif
/* First instantiated from: insights.cpp:18 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct visit_impl<4, Visitor, std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &>
{
static inline void apply(std::size_t index, Visitor &&, std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &)
{
}
};
#endif
/* First instantiated from: insights.cpp:27 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct visit_impl<0, Visitor, std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &>
{
static inline void apply(std::size_t id, Visitor && vis, std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > & var)
{
if(id == 0UL) {
static_cast<const Visitor &&>(std::forward<Visitor>(vis)).operator()(std::get<0UL>(std::forward<std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &>(var)));
} else {
visit_impl<1, Visitor, std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &>::apply(id, std::forward<Visitor>(vis), std::forward<std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &>(var));
}
}
};
#endif
template<std::size_t Idx, typename Visitor, typename Variant>
requires (Idx < std::variant_size_v<std::remove_reference_t<Variant> >)
struct visit_impl<Idx, Visitor, Variant>
{
static inline void apply(std::size_t id, Visitor && vis, Variant && var)
{
if(id == Idx) {
std::forward<Visitor>(vis)(std::get<Idx>(std::forward<Variant>(var)));
} else {
visit_impl<Idx + 1, Visitor, Variant>::apply(id, std::forward<Visitor>(vis), std::forward<Variant>(var));
}
}
};
template<typename Visitor, typename Variant>
void customize_visit(Visitor && vis, Variant && var)
{
std::size_t id = var.index();
visit_impl<0, Visitor, Variant>::apply(id, std::forward<Visitor>(vis), std::forward<Variant>(var));
}
/* First instantiated from: insights.cpp:43 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void customize_visit<Visitor, std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &>(Visitor && vis, std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > & var)
{
std::size_t id = var.index();
visit_impl<0, Visitor, std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &>::apply(id, std::forward<Visitor>(vis), std::forward<std::variant<int, long, double, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &>(var));
}
#endif
通过代码的展开就可以很清晰的明白代码实现的细节。只要把相关的具体的流程搞清楚,对模板的实现就会有一个清醒的认知,这样就不会感觉有什么难点了。
其实,看到上面的代码,大家是不是感觉有一种可以替代的展开方法?前面分析过的std::make_index_sequence是否在也可以达到类似的效果?答案是肯定的,大家感兴趣可以使用它来展开相关的序列进行实现一下。这里就不再赘述了。
当然,上面的是针对一个Variant为例子的简装版,如果想写一些适合于多个Variant的应用,就相对要复杂一些。但只要大家明白了一个,就知道了其内在的工作机制,多个的拓展也就顺理成章的事情了。
四、总结
其实类似手动实现这种库的"手搓"轮子的方法,其实真实的工程应用意义并不多大。更多的意义是让开发者自己对相关的模板技术从底层真正的掌握起来。对技术的掌握,不是单独的一个点,是从点到线到面,到融合整体把握。就如现在到处都在讲,这是一个体系。