14

std::variant可以进入一种叫做“异常无价值”的状态。

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

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

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

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

4

3 回答 3

17

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

  • 一个T
  • 空的

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

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

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

variant的问题来自没有那么容易恢复的状态。


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

于 2019-08-28T16:00:12.523 回答
8

std::optional很容易:

  1. 它包含一个值并分配了一个新值:
    简单,只需委托给赋值运算符并让它处理它。即使在异常的情况下,仍然会留下一个值。

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

  3. 它不包含任何值并且被赋值:
    面对构造异常时恢复为无值很简单。

  4. 它不包含任何值,也没有分配任何值:
    微不足道。

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

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

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

于 2019-08-28T16:05:16.980 回答
5

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

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

于 2019-08-28T16:03:14.430 回答