为什么 std::optional 永远不会 "valueless by exception"?

How is std::optional never "valueless by exception"?

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

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

std::optional,然而,没有这样的状态。 cppreference 做出了大胆的声明:

If an exception is thrown, the initialization state of *this ... is unchanged, i.e. if the object contained a value, it still contains a value, and the other way round.

std::optional 如何避免变得“异常无价值”,而 std::variant 却不能?

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

  • 一个T

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

但是对于optional,从T过渡到空只是一种破坏。因此,只有在 T 的析构函数抛出时才会抛出,真正关心这一点的人。从 empty 过渡到 T 不是问题 - 如果抛出,很容易恢复原始对象:empty 状态为 empty。

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

variant 的问题是无法轻松恢复到这种状态。


1 正如 boost::variant2 那样。
2boost::variant 一样。
3 我不确定执行此操作的变体实现,但有一个设计建议 variant<monostate, A, B> 可以转换到 monostate 状态,如果它持有A 并过渡到 B 抛出。

"valueless by exception"指的是需要改变variant中存储的类型的特定场景。这必然需要 1) 销毁旧值,然后 2) 在其位置创建新值。如果 2) 失败,您将无法返回(没有委员会无法接受的过度开销)。

optional没有这个问题。如果对它包含的对象的某些操作抛出异常,那就这样吧。对象还在。这并不意味着对象的状态仍然有意义 - 它是抛出操作留下的任何内容。希望该操作至少有基本保证。

std::optional 很简单:

  1. 它包含一个值并分配了一个新值:
    简单,只需委托给赋值运算符并让它处理即可。即使出现异常,也会有剩余的值。

  2. 它包含一个值并且该值被删除:
    很简单,dtor 不能扔。标准库通常假定对于用户定义的类型。

  3. 不包含值,赋值1:
    面对构造异常时恢复为无值非常简单。

  4. 它不包含任何值,也没有赋值:
    琐碎。

std::variant 当存储的类型不变时,同样容易。
不幸的是,当分配一个不同的类型时,它必须通过破坏以前的值来为它腾出空间,然后构造新值可能会抛出!

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

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