方式std::variant
当 std::visit
时分派(dispatch)到不同的访问者方法当变体替代品是完全不同的类型时,被调用是非常合理的。本质上是特定于访问者的 vtable
在编译时构建,经过一些错误检查1,通过基于当前 index()
索引表来查找适当的访问者函数。在大多数平台上解析为间接跳转之类的东西。
但是,如果替代方案共享一个公共(public)基类,则调用(非虚拟)成员函数或使用访问者访问基类上的状态在概念上要简单得多:您总是调用相同的方法,并且通常使用相同的指针 2基类。
尽管如此,实现最终还是一样缓慢。例如:
#include <variant>
struct Base {
int m_base;
int getBaseMember() { return m_base; }
};
struct Foo : public Base {
int m_foo;
};
struct Bar : public Base {
int m_bar;
};
using Foobar = std::variant<Foo,Bar>;
int getBaseMemVariant(Foobar& v) {
return std::visit([](auto&& e){ return e.getBaseMember(); }, v);
}
最新版本的
gcc
在 x86 上生成的代码和 clang
是similar3(clang显示):getBaseMemVariant(std::__1::variant<Foo, Bar>&): # @getBaseMemVariant(std::__1::variant<Foo, Bar>&)
sub rsp, 24
mov rax, rdi
mov ecx, dword ptr [rax + 8]
mov edx, 4294967295
cmp rcx, rdx
je .LBB0_2
lea rdx, [rsp + 8]
mov qword ptr [rsp + 16], rdx
lea rdi, [rsp + 16]
mov rsi, rax
call qword ptr [8*rcx + decltype(auto) std::__1::__variant_detail::__visitation::__base::__visit_alt<std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>, std::__1::__variant_detail::__impl<Foo, Bar>&>(std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__impl<Foo, Bar>&)::__fmatrix]
add rsp, 24
ret
.LBB0_2:
mov edi, 8
call __cxa_allocate_exception
mov qword ptr [rax], vtable for std::bad_variant_access+16
mov esi, typeinfo for std::bad_variant_access
mov edx, std::exception::~exception()
mov rdi, rax
call __cxa_throw
decltype(auto) std::__1::__variant_detail::__visitation::__base::__dispatcher<0ul>::__dispatch<std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&>(std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&): # @"decltype(auto) std::__1::__variant_detail::__visitation::__base::__dispatcher<0ul>::__dispatch<std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&>(std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&)"
mov eax, dword ptr [rsi]
ret
decltype(auto) std::__1::__variant_detail::__visitation::__base::__dispatcher<1ul>::__dispatch<std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&>(std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&): # @"decltype(auto) std::__1::__variant_detail::__visitation::__base::__dispatcher<1ul>::__dispatch<std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&>(std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&)"
mov eax, dword ptr [rsi]
ret
decltype(auto) std::__1::__variant_detail::__visitation::__base::__visit_alt<std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>, std::__1::__variant_detail::__impl<Foo, Bar>&>(std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__impl<Foo, Bar>&)::__fmatrix:
.quad decltype(auto) std::__1::__variant_detail::__visitation::__base::__dispatcher<0ul>::__dispatch<std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&>(std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&)
.quad decltype(auto) std::__1::__variant_detail::__visitation::__base::__dispatcher<1ul>::__dispatch<std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&>(std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&)
call qword ptr [8*rcx + ...
是对 vtable 指向的函数的实际间接调用(vtable 本身出现在列表的底部)。之前的代码是先检查“为空”状态,然后设置visit
调用(我不确定 rdi
的奇怪之处是什么,我猜它正在设置一个指向访问者的指针作为第一个参数或其他东西)。由 vtable 指针指向并由
call
执行的实际方法很简单,单mov
读取成员。至关重要的是,两者都是相同的: mov eax, dword ptr [rsi]
ret
所以我们有一个巨大的困惑。执行那个单
mov
我们有十几个设置指令,更重要的是一个间接分支:如果针对一系列 Foobar
variant
具有不同包含选项的对象将错误预测非常糟糕。最后,间接调用似乎是进一步优化的一个不可逾越的障碍:这里将看一个没有任何周围上下文的简单调用,但在实际使用中,这可能会被优化为一个更大的函数,有很大的进一步优化机会 - 但我认为间接调用调用将阻止它。您可以 play with the code yourself on godbolt .
对阵 union
缓慢不是固有的:这是一个非常简单的“歧视 union ”
struct
将这两个类组合在一个 union
中连同 isFoo
跟踪包含哪些类的鉴别器:struct FoobarUnion {
bool isFoo;
union {
Foo foo;
Bar bar;
};
Base *asBase() {return isFoo ? (Base *)&foo : &bar; };
};
int getBaseMemUnion(FoobarUnion& v) {
return v.asBase()->getBaseMember();
}
对应的
getBaseMemUnion
函数编译为单个 mov
关于 gcc 和 clang 的说明:getBaseMemUnion(FoobarUnion&): # @getBaseMemUnion(FoobarUnion&)
mov eax, dword ptr [rdi + 4]
ret
当然,受歧视 union 不必检查“无值(value)”错误条件,但这不是
variant
的主要原因。 Foo
在任何情况下都不可能出现这种情况。和 Bar
因为他们的构造函数都没有抛出4。即使你想支持这样的状态,结果函数用 union
是 still very efficient - 只添加了一个小检查,但调用基类的行为是相同的。我可以对
variant
的这种使用做些什么吗?在调用公共(public)基类函数的情况下是有效的,还是零成本抽象的 promise 在这里没有实现?我对不同的调用模式、编译器选项等持开放态度。
1 特别是检查变体是否为
valueless_by_exception
由于先前的分配失败。2 指向基类的指针并不总是与所有替代项的最派生指针具有相同的关系,例如,当涉及多重继承时。
3 井
gcc
有点糟糕,因为它似乎在调用 visit
之前预先执行了“无值(value)”检查。以及 vtable
指向的每个自动生成的方法中. clang 只做前期工作。请记住,当我说“gcc”时,我的意思是“gcc with libstdc++”,而“clang”的真正意思是“clang with libc++”。一些差异,比如多余的 index()
checkin 生成的访问者函数可能是由于库差异而不是编译器优化差异。4 如果
valueless
状态有问题,你也可以考虑像 strict_variant
这样的东西它永远不会有一个空状态,但如果移动构造函数不能抛出,它仍然使用本地存储。
最佳答案
对于它的值(value),完全手动访问 switch
做得很好:
// use a code generator to write out all of these
template <typename F, typename V>
auto custom_visit(F f, V&& v, std::integral_constant<size_t, 2> )
{
switch (v.index()) {
case 0: return f(std::get<0>(std::forward<V>(v)));
case 1: return f(std::get<1>(std::forward<V>(v)));
#ifdef VALUELESS
case std::variant_npos: {
[]() [[gnu::cold, gnu::noinline]] {
throw std::bad_variant_access();
}();
}
#endif
}
__builtin_unreachable();
}
template <typename F, typename V>
auto custom_visit(F f, V&& v) {
return custom_visit(f, std::forward<V>(v),
std::variant_size<std::decay_t<V>>{});
}
你会使用像:
int getBaseMemVariant2(Foobar& v) {
return custom_visit([](Base& b){ return &b; }, v)->getBaseMember();
}
与
VALUELESS
,这会发出:getBaseMemVariant2(std::variant<Foo, Bar>&):
movzx eax, BYTE PTR [rdi+8]
cmp al, -1
je .L27
cmp al, 1
ja .L28
mov eax, DWORD PTR [rdi]
ret
.L27:
sub rsp, 8
call auto custom_visit<getBaseMemVariant2(std::variant<Foo, Bar>&)::{lambda(Base&)#1}, std::variant<Foo, Bar>&>(getBaseMemVariant2(std::variant<Foo, Bar>&)::{lambda(Base&)#1}, std::variant<Foo, Bar>&, std::integral_constant<unsigned long, 2ul>)::{lambda()#1}::operator()() const [clone .isra.1]
这很好。无
VALUELESS
,这会发出:getBaseMemVariant2(std::variant<Foo, Bar>&):
mov eax, DWORD PTR [rdi]
ret
如预期的。
我真的不知道从中得出什么结论,如果有的话。很明显,还有希望吗?
关于c++ - 有没有希望有效地调用 std::variant 上的公共(public)基类方法?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47383563/