我很好奇 std::variant 是如何工作的。
据我了解,它“基本上是”带有附加索引变量的 union 。 然而,我很困惑它如何确定将构造/分配有效地转发到哪种类型。我通过 godbolt.org 查看过它,它似乎确实非常有效,即使是从另一个变体复制时也是如此。
我已经考虑过如何实现类似变体的类型,但我想到的唯一方法是拥有某种辅助函数,将构造函数/分配/析构函数调用转发到基于一个索引参数。然而,这对于递归调用来说效率相当低,不是吗?
另一种方法是拥有一个具有固定类型的 union ,并且每当需要使用构造函数/析构函数等时。一个大的开关将根据索引变量决定使用哪个 union 成员,但这需要大量的代码膨胀并且仅适用于固定类型。
从我在 godbolt.org 上看到的情况来看,variant 的 gcc 实现调用了 std::__detail::__variant::__gen_vtable_impl
,但我还没有找到任何关于它的作用的信息.
此外,例如,如果它们都可以从传递的参数类型“转换”,那么它如何确定在构造期间使用哪种变体类型? cppreference 表示类型必须是可转换的,但是如果有多种可能的可转换变体类型怎么办?
我尝试查看 gcc 的 stdlib 实现源代码,但它非常令人困惑,所以如果有人解释它,我将不胜感激。
最佳答案
Another way would be to have a union with fixed types and whenever it would need to use a constructor/destructor etc. a big switch would decide which union member to use depending on the index variable, but that would require a lot of code bloat and would only work for fixed types.
但这(道德上)对于任意类型列表都适用。一个switch
具有连续非重叠case
s 在逻辑上只是索引到程序代码中。因此您可以直观地替换 switch
使用数组查找:提取 case
你会switch
进入 as 函数,然后 switch
正在访问case
s 表示索引数组并调用结果。
switch(tag) { // effectively "go to line here + tag", so conceptually a lot like indexing
case 0: ret = ...; break;
case 1: ret = ...; break;
...
}
// <=>
ret_t case0(args_t args) { return ...; }
ret_t case1(args_t args) { return ...; }
...
ret_t (*cases[])(args_t) = {case0, case1, ...};
ret = cases[tag](args); // and now it's literally indexing
但是生成一堆函数并根据模式构造表达式正是模板的作用。
template<std::size_t I, typename F, typename... Ts>
decltype(auto) visit_case(F &f, std::variant<Ts...> &v) {
return f(std::get<I>(v));
}
// visit_case<0, F, T1, T2, ...>(f, v) assumes v is a T1 t1 and calls f(t1);
// visit_case<1, F, T1, T2, ...>(f, v) assumes v is a T2 t2 and calls f(t2);
// etc., so this template is capable of generating all the cases of a function on a variant as separate functions
template<typename F, typename... Ts, std::size_t... Is>
constexpr auto visit_cases_impl(std::index_sequence<Is...>) {
return std::array{visit_case<Is, F, Ts...>...};
}
template<typename F, typename... Ts>
constexpr inline auto visit_cases =
visit_cases_impl<F, Ts...>(std::index_sequence_for<Ts...>{});
// visit_cases<F, T1, T2, T3> =
// std::array{visit_case<0, F, T1, T2, T3>, visit_case<1, F, T1, T2, T3>, visit_case<2, F, T1, T2, T3>}
您遇到的“vtable
”指的是函数指针数组,表示如何处理可能的替代方案。这张表是您基本所需的全部 std::visit
:
template<typename F, typename... Ts>
decltype(auto) visit(F f, std::variant<Ts...> &v) {
return visit_cases<F, Ts...>[v.index()](f, v);
// <=>
// switch(v.index()) {
// case 0: return f(std::get<0>(v));
// case 1: return f(std::get<1>(v));
// ...
// }
}
现在,为了实现std::variant
,我认为需要有一些super_visit
还将变体索引作为模板参数传递给 f
,但除此之外它应该相对简单。复制/移动构造函数/赋值和析构函数都可以编写为此类索引访问。 A complete example.
关于c++ - c++17 std::variant 如何确定使用哪种类型?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/69332236/