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.
但这(在道德上)适用于任意类型列表。具有连续非重叠 case
的 switch
在逻辑上只是索引到您的程序代码中。因此,您可以直观地将 switch
替换为数组查找:将 case
提取为函数,然后 switch
ing 在 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
”指的是函数指针数组,表示如何处理可能的替代方案。这个 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.
我很好奇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.
但这(在道德上)适用于任意类型列表。具有连续非重叠 case
的 switch
在逻辑上只是索引到您的程序代码中。因此,您可以直观地将 switch
替换为数组查找:将 case
提取为函数,然后 switch
ing 在 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
”指的是函数指针数组,表示如何处理可能的替代方案。这个 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.