为什么 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
那样。
2 与 boost::variant
一样。
3 我不确定执行此操作的变体实现,但有一个设计建议 variant<monostate, A, B>
可以转换到 monostate
状态,如果它持有A
并过渡到 B
抛出。
"valueless by exception"指的是需要改变variant中存储的类型的特定场景。这必然需要 1) 销毁旧值,然后 2) 在其位置创建新值。如果 2) 失败,您将无法返回(没有委员会无法接受的过度开销)。
optional
没有这个问题。如果对它包含的对象的某些操作抛出异常,那就这样吧。对象还在。这并不意味着对象的状态仍然有意义 - 它是抛出操作留下的任何内容。希望该操作至少有基本保证。
std::optional
很简单:
它包含一个值并分配了一个新值:
简单,只需委托给赋值运算符并让它处理即可。即使出现异常,也会有剩余的值。
它包含一个值并且该值被删除:
很简单,dtor 不能扔。标准库通常假定对于用户定义的类型。
不包含值,赋值1:
面对构造异常时恢复为无值非常简单。
它不包含任何值,也没有赋值:
琐碎。
std::variant
当存储的类型不变时,同样容易。
不幸的是,当分配一个不同的类型时,它必须通过破坏以前的值来为它腾出空间,然后构造新值可能会抛出!
之前的值已经丢失了,怎么办?
将其标记为 无值异常 以获得稳定、有效但不受欢迎的状态,并让异常传播。
可以使用额外的 space 和时间来动态分配值,将旧值临时保存在某个地方,在分配之前构造新值等等,但是所有这些策略都是昂贵的,而且只有第一种总是有效。
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
那样。
2 与 boost::variant
一样。
3 我不确定执行此操作的变体实现,但有一个设计建议 variant<monostate, A, B>
可以转换到 monostate
状态,如果它持有A
并过渡到 B
抛出。
"valueless by exception"指的是需要改变variant中存储的类型的特定场景。这必然需要 1) 销毁旧值,然后 2) 在其位置创建新值。如果 2) 失败,您将无法返回(没有委员会无法接受的过度开销)。
optional
没有这个问题。如果对它包含的对象的某些操作抛出异常,那就这样吧。对象还在。这并不意味着对象的状态仍然有意义 - 它是抛出操作留下的任何内容。希望该操作至少有基本保证。
std::optional
很简单:
它包含一个值并分配了一个新值:
简单,只需委托给赋值运算符并让它处理即可。即使出现异常,也会有剩余的值。它包含一个值并且该值被删除:
很简单,dtor 不能扔。标准库通常假定对于用户定义的类型。不包含值,赋值1:
面对构造异常时恢复为无值非常简单。它不包含任何值,也没有赋值:
琐碎。
std::variant
当存储的类型不变时,同样容易。
不幸的是,当分配一个不同的类型时,它必须通过破坏以前的值来为它腾出空间,然后构造新值可能会抛出!
之前的值已经丢失了,怎么办?
将其标记为 无值异常 以获得稳定、有效但不受欢迎的状态,并让异常传播。
可以使用额外的 space 和时间来动态分配值,将旧值临时保存在某个地方,在分配之前构造新值等等,但是所有这些策略都是昂贵的,而且只有第一种总是有效。