std::variant
可以进入一种叫做“异常无价值”的状态。
据我了解,这种情况的常见原因是移动分配引发异常。变体的旧值不再保证存在,预期的新值也不再存在。
std::optional
但是,没有这样的状态。cppreference 提出了大胆的主张:
如果抛出异常,*this ... 的初始化状态不变,即如果对象包含一个值,它仍然包含一个值,反之亦然。
如何std::optional
能够避免成为“异常无价值”,而std::variant
不是?
std::variant
可以进入一种叫做“异常无价值”的状态。
据我了解,这种情况的常见原因是移动分配引发异常。变体的旧值不再保证存在,预期的新值也不再存在。
std::optional
但是,没有这样的状态。cppreference 提出了大胆的主张:
如果抛出异常,*this ... 的初始化状态不变,即如果对象包含一个值,它仍然包含一个值,反之亦然。
如何std::optional
能够避免成为“异常无价值”,而std::variant
不是?
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
并且转换为B
throw。
std::optional
很容易:
它包含一个值并分配了一个新值:
简单,只需委托给赋值运算符并让它处理它。即使在异常的情况下,仍然会留下一个值。
它包含一个值并且该值被删除:
简单,dtor 不能抛出。标准库通常假设用户定义的类型。
它不包含任何值并且被赋值:
面对构造异常时恢复为无值很简单。
它不包含任何值,也没有分配任何值:
微不足道。
std::variant
在存储的类型不变的情况下具有相同的轻松时间。
不幸的是,当分配了不同的类型时,它必须通过破坏先前的值来为它腾出位置,然后构造新值可能会抛出!
由于之前的值已经丢失,你能做什么?
通过异常将其标记为无价值以具有稳定、有效但不受欢迎的状态,并让异常传播。
可以使用额外的空间和时间来动态分配值,将旧值临时保存在某处,在分配之前构造新值等等,但所有这些策略都是昂贵的,而且只有第一个总是有效的。
“异常无价值”是指您需要更改存储在变体中的类型的特定场景。这必然需要 1) 销毁旧值,然后 2) 在其位置创建新值。如果 2) 失败,您将无法返回(没有委员会无法接受的过度开销)。
optional
没有这个问题。如果对它包含的对象的某些操作引发异常,那就这样吧。物体还在。这并不意味着对象的状态仍然有意义——它是投掷操作留下的任何东西。希望该操作至少有基本的保证。