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
的问题是没有那么容易恢复到的状态。
1 和 boost::variant2
一样。2 和 boost::variant
一样。3 我不确定的变体实现,但有一个设计建议,如果 variant<monostate, A, B>
持有 A
并转换到 B
时,它可以转换到 monostate
状态。
【讨论】:
我看不出这个答案如何解决optional<T>
从 T
到不同的 T
状态的情况。注意emplace
和operator=
在进程中抛出异常的情况下会有不同的行为!
@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 的第二个重载