c++17 std::variant 如何确定使用哪种类型?

How does c++17 std::variant determine which type to use?

我很好奇std::variant是如何工作的。

根据我的理解,它是“基本上”一个带有附加索引变量的联合。 但是,我很困惑它如何确定将 construction/assignment 有效地转发到哪种类型。我已经通过 godbolt.org 查看了它,它看起来确实非常有效,即使是从另一个变体复制时也是如此。

我已经考虑过如何实现类似变体的类型,但我想到的唯一方法是使用某种辅助函数将 constructor/assignemnt/destructor 调用转发到适当的类型基于索引参数。然而,递归调用效率很低,不是吗?

另一种方法是使用固定类型的联合,并且每当它需要使用 constructor/destructor 等时。一个大开关将根据索引变量决定使用哪个联合成员,但那样会需要大量代码膨胀,并且只适用于固定类型。

根据我在 godbolt.org 上看到的内容,变体的 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.

但这(在道德上)适用于任意类型列表。具有连续非重叠 caseswitch 在逻辑上只是索引到您的程序代码中。因此,您可以直观地将 switch 替换为数组查找:将 case 提取为函数,然后 switching 在 cases 表示索引数组并调用结果。

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”指的是函数指针数组,表示如何处理可能的替代方案。这个 table 是你需要的基本 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,否则它应该相对直截了当。 copy/move constructors/assignments 和析构函数都可以写成这样的索引访问。 A complete example.