std::optional 如何永远不会“异常无价值”?

Posted

技术标签:

【中文标题】std::optional 如何永远不会“异常无价值”?【英文标题】:How is std::optional never "valueless by exception"? 【发布时间】:2020-01-01 21:42:42 【问题描述】:

std::variant 可以进入一个名为“valueless by exception”的状态。

据我了解,造成这种情况的常见原因是移动分配引发了异常。变体的旧值不再保证存在,预期的新值也不再存在。

std::optional,然而,没有这样的状态。 cppreference 提出了大胆的主张:

如果抛出异常,*this ...的初始化状态不变,即如果对象包含值,它仍然包含值,反之亦然。

std::optional 如何避免成为“异常无价值”,而std::variant 则不然?

【问题讨论】:

【参考方案1】:

optional<T> 具有以下两种状态之一:

T

A variant 只能在从一种状态转换到另一种状态时进入无价值状态,如果转换会抛出 - 因为您需要以某种方式恢复原始对象,并且这样做的各种策略需要额外的存储1、堆分配2或空状态3

但是对于optional,从T 过渡到空只是一种破坏。所以只有在T 的析构函数抛出时才会抛出,而且那时真的谁在乎。并且从空转换到 T 不是问题 - 如果抛出,很容易恢复原始对象:空状态是空的。

具有挑战性的案例是:emplace(),而我们已经有了T。我们必然需要销毁原始对象,那么如果 emplace 构造抛出,我们该怎么办?使用optional,我们有一个已知的、方便的空状态可以回退——所以设计就是为了做到这一点。

variant 的问题是没有那么容易恢复到的状态。


1boost::variant2 一样。2boost::variant 一样。3 我不确定的变体实现,但有一个设计建议,如果 variant<monostate, A, B> 持有 A 并转换到 B 时,它可以转换到 monostate 状态。

【讨论】:

我看不出这个答案如何解决 optional<T>T 到不同的 T 状态的情况。注意emplaceoperator=在进程中抛出异常的情况下会有不同的行为! @MaxLanghof:如果构造函数抛出emplace,则optional 被明确声明为未使用。如果operator= 在构造过程中抛出,那么同样没有任何价值。 Barry 的观点仍然有效:它之所以有效,是因为optional 总是可以进入一个合法的空状态。 variant 没有那么奢侈,因为variant 不能为空。 @NicolBolas 困难的情况(也是与variant 问题最相似的情况)是当您有一个现有值时分配一个新值。而保持初始化状态的核心是使用T::operator=——这个特定的案例不涉及空的optional,也没有析构函数。由于此答案中涵盖的有关std::optional 的所有案例都涉及破坏或空状态,因此我认为缺少此重要案例(由其他答案涵盖)。不要误会我的意思,这个答案涵盖了所有其他方面就好了,但我必须自己阅读最后一个案例...... @MaxLanghof 与optional 有什么关系?它只是做类似**this = *other 的事情。 @L.F.这是重要的细节——它不会破坏并重新创建包含的实例,这与std::variant(或std::optional::emplace)不同。但我认为这归结为规范的哪些部分是显而易见的,哪些部分还有待解释。这里的答案在这方面有所不同,应该涵盖界面的不同可能的先入之见。【参考方案2】:

“异常无价值”是指需要更改存储在变体中的类型的特定场景。这必然需要 1) 破坏旧值,然后 2) 在其位置创建新值。如果 2) 失败,您将无法返回(没有委员会无法接受的过度开销)。

optional 没有这个问题。如果对它包含的对象的某些操作引发异常,那就这样吧。物体还在。这并不意味着对象的状态仍然有意义——它是投掷操作留下的任何东西。希望该操作至少有基本的保证。

【讨论】:

“*this 的初始化状态没有改变”……我误解了那个说法吗?我想你是说它可能会变成没有意义的东西。 optional 的角度来看,它仍然持有一个对象。该对象是否处于可用状态不是optional 关心的问题。 std::optional::operator= 使用T::operator= 而不是破坏+ 构造T 值是一个相当重要的细节。 emplace 执行后者(如果新值的构造抛出,则将 optional 留空)。【参考方案3】:

std::optional 很简单:

    它包含一个值并分配了一个新值: 很简单,只需委托给赋值运算符并让它处理它。即使出现异常,也会留下一个值。

    它包含一个值,该值被删除: 很简单,dtor 不能扔。标准库通常假定用户定义的类型是这样的。

    它不包含任何值,并且分配了一个值: 在构造异常的情况下恢复为无值很简单。

    它不包含任何值,也没有赋值: 微不足道。

std::variant 在存储类型不变的情况下也有同样的轻松时间。 不幸的是,当分配了不同的类型时,它必须通过销毁先前的值来为它腾出位置,然后构造新值可能会抛出!

由于之前的值已经丢失了,怎么办? 将其标记为异常无价值以具有稳定、有效但不受欢迎的状态,并让异常传播。

人们可以使用额外的空间和时间来动态分配值,将旧值临时保存在某个地方,在分配之前构造新值等等,但所有这些策略都是昂贵的,而且只有第一个总是有效的。

【讨论】:

以上是关于std::optional 如何永远不会“异常无价值”?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Xcode 中获得 std::optional 支持?

如何优雅处理多参数返回/无参数返回——std::optional

如何在 if 语句中最好地测试和解包 std::optional

如何使用 std::optional<T>::emplace 的第二个重载

为啥 std::optional operator* 没有 has_value() 的调试模式断言?

std::optional 成员是不是连续存储?