【问题标题】:Why std::get for variant throws on failure instead of being undefined behaviour?为什么 std::get for variant 会引发失败而不是未定义的行为?
【发布时间】:2018-07-26 18:58:36
【问题描述】:

根据cppreference std::get for variant 如果variant 中包含的类型不是预期的类型,则抛出std::bad_variant_access。这意味着标准库必须检查每个访问 (libc++)

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

【问题讨论】:

  • @Justin 我不认为这是一个真正的重复。没有“为什么”的答案。其次,对于“我可以解决它”实际上没有答案。我正在提名重新开放的问题。
  • 因为这就是std::variant for:'类型安全的联合'。如果您不希望它是类型安全的,或者想要 UB,请不要使用它:使用 union
  • In this thread,有些人给出了为什么std::variant 可能没有std::unchecked_get 的一些动机。我不知道这是否真的是标准会议上讨论的内容,但推理背后有逻辑
  • @MarquisofLorne 那一半的STL也要去掉,因为到处都有UB,你可以自己实现。

标签: c++ variant c++17


【解决方案1】:

为什么它不是未定义的行为,就像 c++ 中的其他地方一样?我可以解决它吗?

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

类模板 std::variant 表示类型安全的联合。

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

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

做出这个决定的理由是什么?

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

【讨论】:

  • 你是认真的吗?如果用union 替换variant 这么简单,为什么会有前者存在?
  • 我认为这很清楚——创建union 的类型安全版本。在 C 中编写 unions 时,我记得总是将 union 对象与 intenum 配对,以便我可以存储有关在联合中设置了哪种类型的信息。否则,如果我从错误的数据成员中读取,我会冒 UB 的风险。在这里,std::variant 为联合类型对象提供错误处理和类型调用,因此您不必自己实现(除了检查类型是否正确)。
  • 我不明白反对意见。 variantunion 加上类型安全;那么,variant 减去类型安全性自然是一个联合。我必须赞成这个答案,但@DanielDay 请稍微修正一下语言。您的意思是说“如果您不想要类型安全,请使用普通联合而不是变体”,但实际上您在第一句话中说相反。
  • @kkm 如果您只关心存储ints、floats 等,那么这很容易替换。但是,如果其中一种选择是std::string,或者任何其他重要的类型呢?你最终会得到类似this 的东西。您还会失去其他不错的功能,例如访问。是的,您可以自己编写所有内容,但这可以作为任何要求另一种做事方式的问题的答案。
  • @kkm: 不是非黑即白;通过切换到union,除了类型检查之外,您还放弃了很多。不过,我同意 Daniel 的观点,如果您真的非常渴望,这至少是一个有效的解决方法。
【解决方案2】:

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

但是,您可以通过编写*std::get_if&lt;T&gt;(&amp;variant) 来接近所需的行为。如果当时variant 不持有Tstd::get_if&lt;T&gt; 返回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

它正在比较变体的索引并在索引正确时有条件地移动返回值。即使索引不正确会导致 UB,clang 目前无法优化比较。

有趣的是,返回 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()__assume 来帮助编译器,但是当您这样做时,gcc 目前是唯一的编译器capable of removing the checks

【讨论】:

  • 您是否在查看your own exampleget_int() 的另一个反编译重载?反汇编线 1 处的基于变体的版本确实会发出标签检查。标记的联合一没有。 \n 另请注意,Praetorian 对 the other answer 的 cmets 中的非平凡可构造类型有很好的看法。
  • @kkm 是的。我将标记的工会放在那里进行比较。很明显,带标签的联合会发出更好的代码,但理论上,这个std::get_if 方法应该可以工作。编译器拥有他们需要的所有信息。
  • @kkm 你指的是我的第二个组装块吗?这不在我发布的示例中,但您可以通过将int const&amp; 更改为int 轻松获得它。我加个链接
  • 确实编译器会这样做,而且我也同意编译器假设一些使程序的行为未定义的不变量不会没有帮助(并且,作为旁注,推断此类不变量的一般问题是可能无法确定:))。 \n 是的,添加链接将提高 IMO 答案的可读性和清晰度!
  • 是的,这很有效,我喜欢 *get_if 技巧,谢谢。真的很遗憾,我们不得不这样做。
【解决方案3】:

做出这个决定的理由是什么?

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

std::variant行为的很多灵感来自std::optional的行为,如std::variantP0088的提案中所述:

本提案试图应用从optional...中吸取的经验教训...

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

  • 您不确定当前持有什么
    • optional 中要么是类型要么什么都不是 (nullopt_t)
    • variant 中,它要么是多种类型之一,要么什么都没有(参见valueless_by_exception
  • 对该类型进行操作的所有函数都标记为constexpr
    • 这似乎是巧合或只是良好的设计实践,但很明显,variant 在此方面遵循 optional 的领导(请参阅上面的链接提案)
  • 它们都提供了一种检查空虚的方法
    • std::optional 隐式转换为bool,或者has_value 函数
    • std::variantvalueless_by_exception,它会告诉您变量是否为空,因为构造活动类型会引发异常
  • 它们每个都提供了一种投掷和非投掷访问方式
    • std::optional 的潜在抛出访问权限是 value,它可能会抛出 bad_optional_access
    • std::variant 的潜在抛出访问权限是 get,它可能会抛出 bad_variant_access
    • 如果optional 为空,std::optional 的非抛出(我使用的术语有点松散)访问权限是 value_or,它可能会返回一个替代选项(您传入)
    • std::variant 的非抛出访问是 get_if,如果提供的索引或类型不正确,则返回 nullptr

事实上,这些相似之处是如此刻意,以至于用于optionalvariant 的基类中的不一致是投诉的原因(请参阅this Google Groups discussion

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


至于为什么std::optional 抛出,如果你查看它的提议,N3793 有一个抛出访问器被宣传为对 Boost.Optional 的改进,std::optional 就是从 std::optional 诞生的。我还没有找到任何关于为什么这是一个改进的讨论,所以现在我来推测一下:提供满足两个错误处理阵营(哨兵值与异常)的抛出和非抛出访问器很容易,而且它还有助于从语言中消除一些未定义的行为,这样如果您选择走潜在的路线,就不会不必要地自取其辱。

【讨论】:

  • valueless_by_exception 与空的optional 完全不同。 optional 为空是合法状态;该对象是完全可用的(在其合同范围内)。没有价值的variant 不处于合法状态。它只能通过抛出异常进入该状态,而您几乎无法对毫无价值的variant 做任何事情。没有探视,没有get,什么都没有。
  • 另外,整个论点都失败了,因为optional::value 抛出,optional::operator* 没有
  • @NicolBolas 关于valueless_by_exception 的观点,但“整个论点失败”不是有点极端吗?我觉得该提案清楚地表明该设计的灵感来自optionalvariant 没有可比的 `operator*`,所以很难说那里的行为有分歧
  • @Nicol 非抛出运算符* 是一种类似于 vector::at 与 vector::operator[] 的优化。变体不存在这种优化的事实可能是因为变体首先应该保护您免受无效访问(通过抛出),不是吗?
  • @rubenvb:这是我的观点。选择不进行非检查getoptional 无关;这纯粹与variant的设计有关。
【解决方案4】:

我想我找到了!

似乎可以在proposal 中的“与修订版 5 的差异”下找到原因:

Kona 妥协:f !v.valid(),make get<...>(v) 和 visit(v) throw。

含义 - 变体必须进入“values_by_exception”状态。使用相同的if,我们总是可以抛出。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-01-08
    • 1970-01-01
    • 2019-06-10
    • 2018-08-30
    • 2014-04-13
    • 2011-05-08
    • 2014-01-04
    • 1970-01-01
    相关资源
    最近更新 更多