为什么 std::get for variant 抛出失败而不是未定义的行为?

Why std::get for variant throws on failure instead of being undefined behaviour?

根据cppreference std::get for variant throws std::bad_variant_access if the type contained in the variant is not the expected one. This means that the standard library has to check on every access (libc++)

做出这个决定的理由是什么?为什么它不是未定义的行为,就像 C++ 中的其他地方一样?我可以解决这个问题吗?

Why it's not undefined behavour, like everywhere else in c++? Can I work around it?

是的,有一个直接的解决方法。如果您不想要类型安全,请使用普通的 union 而不是 std::variant。正如您引用的参考文献中所说:

The class template std::variant represents a type-safe union.

union 的目的是拥有一个可以从多种不同类型中获取值的对象。在任何给定时间,只有一种类型的 union 是 'valid',具体取决于分配了哪些成员变量:

union example {
   int i;
   float f;
};

// code block later...
example e;
e.i = 10;
std::cout << e.f << std::endl; // will compile but the output is undefined!

std::variant 概括了 union,同时添加了类型安全以帮助确保您只访问正确的数据类型。如果你不想要这种安全,你总是可以使用 union 来代替。

What was the rational for this decision?

我个人并不知道做出这个决定的理由是什么,但您可以随时查看 the papers from the C++ standardization committee 以深入了解该过程。

std::variant 的当前 API 没有 std::get 的未检查版本。我不知道为什么要这样标准化;我说的任何话都只是猜测。

但是,您可以通过编写 *std::get_if<T>(&variant) 来接近所需的行为。如果当时 variant 不持有 Tstd::get_if<T> returns nullptr,那么取消引用它是未定义的行为。因此,编译器可以假定变体保持 T.


实际上,这不是编译器最容易执行的优化。与简单的标记联合相比,它发出的代码可能没有那么好。以下代码:

int const& get_int(std::variant<int, std::string> const& variant)
{
    return *std::get_if<int>(&variant);
}

发射 this with clang 5.0.0:

get_int(std::variant<int, std::string> const&):
  xor eax, eax
  cmp dword ptr [rdi + 24], 0
  cmove rax, rdi
  ret

它正在比较变体的索引,并在索引正确时有条件地移动 return 值。尽管索引不正确是 UB,但 clang 目前无法优化比较。

有趣的是,return使用 int 而不是引用 optimizes the check away:

int get_int(std::variant<int, std::string> const& variant)
{
    return *std::get_if<int>(&variant);
}

发出:

get_int(std::variant<int, std::string> const&):
  mov eax, dword ptr [rdi]
  ret

您可以在这样做时使用 __builtin_unreachable() or __assume, but gcc is currently the only compiler capable of removing the checks 来帮助编译器。

What was the rationale for this decision?

这种问题总是很难回答,但我会试一试。

很多std::variant came from the behavior of std::optional, as stated in the proposal for std::variant, P0088行为的灵感:

This proposal attempts to apply the lessons learned from optional...

您可以看到这两种类型之间的相似之处:

  • 您不确定当前持有什么
    • optional 中要么是类型要么什么都不是 (nullopt_t)
    • variant 中,它要么是多种类型中的一种,要么什么都不是(参见 valueless_by_exception
  • 对该类型进行操作的所有函数都已标记constexpr
    • 这可能看起来是巧合或只是良好的设计实践,但很明显 variant 在这方面遵循 optional 的领导(参见上面的链接提案)
  • 它们各自提供了一种检查是否为空的方法
    • std::optional 隐式转换为 bool,或者 has_value 函数
    • std::variantvalueless_by_exception 告诉你变量是否为空,因为构造活动类型抛出异常
  • 它们各自提供一种投掷和 non-throwing 访问的方式
    • Potentially-throwing std::optional 的访问权限是 value and it may throw bad_optional_access
    • Potentially-throwing std::variant 的访问权限是 get and it may throw bad_variant_access
    • Non-throwing(我使用这个术语有点宽松)std::optional 的访问权限是 value_or 如果optional 为空
    • Non-throwing std::variant 的访问权限是 get_if 如果提供的索引或类型错误,则 return 是 nullptr

事实上,相似之处是如此有意,以至于用于 optionalvariant 的基数 类 的不一致引起了投诉(参见 this Google Groups discussion

所以为了回答你的问题,它抛出是因为 optional 抛出 。请记住,应该很少遇到投掷行为;您应该使用带有变体的访问者模式,即使您确实调用 get 它也只会在您为它提供类型列表大小的索引或请求的类型不是活动类型时抛出。所有其他误用都被视为 ill-formed 并且应该发出编译器错误。


至于为什么 std::optional 抛出,如果你检查它的提案,N3793 having a throwing accessor was advertised as an improvement over Boost.Optionalstd::optional 就是从中诞生的。我还没有找到关于为什么这是一个改进的任何讨论,所以现在我推测:提供满足 error-handling 阵营的 throwing 和 non-throwing 访问器很容易(哨兵值与异常),并且它还有助于从语言中去除一些未定义的行为,因此如果您选择走 potentially-throwing 路线,就不会不必要地搬起石头砸自己的脚。

我想我找到了!

似乎可以在 proposal 中的 "Differences to revision 5" 下找到原因:

The Kona compromise: f !v.valid(), make get<...>(v) and visit(v) throw.

意思 - 变体必须进入 "values_by_exception" 状态。使用相同的 if 我们总是可以抛出。

即使知道这个合理性,我个人也想避免这个检查。 *get_if 从贾斯汀的回答中解决的问题对我来说似乎已经足够好了(至少对于库代码而言)。