std::optional如何避免“因异常而无值”?

14

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

据我所知,这种情况的常见原因是移动赋值引发异常。变体(variant)的旧值不再保证存在,预期的新值也不再存在。

std::optional则没有这种状态。cppreference对此做出了大胆声明:

如果引发异常,则*this的初始化状态…保持不变,即如果对象包含一个值,则仍然包含一个值,反之亦然。

std::optional如何避免变成“valueless by exception”,而std::variant没有呢?

3个回答

18

optional<T>有两种状态:

  • 一个T

只有当从一种状态转换到另一种状态时,variant才能进入没有值的状态 - 因为您需要以某种方式恢复原始对象,并且为此采用的各种策略需要额外的存储1、堆分配2或空状态3

但对于optional,从T转换为空仅仅是一个销毁过程。因此,如果T的析构函数没有抛出异常,在此过程中就不会抛出异常。如果从空转换到T也不是问题-如果抛出异常,也很容易恢复原始对象:空状态为空。

具有挑战性的情况是:emplace()在我们已经有一个T的情况下。我们必须销毁原始对象,因此如果emplace构造函数抛出异常,我们该怎么办?使用optional,我们有一个已知的、方便的空状态回退选项 - 因此设计就是这样做的。

variant由于没有这个易于恢复的状态而引起问题。


1boost::variant2所做的那样。
2boost::variant所做的那样。
3我不确定有哪个变体实现可以这样做,但有一个设计建议是如果variant<monostate, A, B>持有一个A并且向B的转换抛出异常,则可以转换为monostate状态。


我不明白这个答案如何处理 optional<T>T 转换到另一个 T 状态的情况。请注意,在过程中如果抛出异常,emplaceoperator= 的行为是不同的! - Max Langhof
如果emplace中的构造函数抛出异常,那么将显式声明optional未参与。如果operator=在构造期间抛出异常,那么同样没有值。Barry的观点仍然有效:它之所以有效是因为始终存在一个合法的空状态,可以将optional设置为空。variant没有这种奢侈,因为variant不能为空。 - Nicol Bolas
@NicolBolas 最困难的情况(也是最类似于“variant”问题的情况)是在已有值的情况下分配一个新值。而保留初始化状态的核心就是使用 T::operator= - 这种特定情况不涉及空的 optional 或者任何析构函数。由于此答案中涉及 std::optional 的所有案例都涉及销毁或者空状态,因此我认为这个重要案例(其他答案都没有涉及)被遗漏了。别误会,这个答案完全涵盖了其他所有方面,但我自己还必须阅读这个最后一个案例... - Max Langhof
@MaxLanghof 这跟“optional”有什么关系?它只是做了类似于**this = *other的事情。 - L. F.
@L.F. 这是一个重要的细节 - 它不会像std::variant(或std::optional::emplace)一样破坏并重新创建包含的实例。但我认为这取决于人们认为哪些规范部分是显而易见的,以及什么需要解释。这里的答案在这方面有所不同,它应该涵盖接口可能存在的不同预设。 - Max Langhof

8

std::optional很简单:

  1. 它包含一个值并且要赋新值:
    很容易,只需委派给赋值运算符来处理。即使出现异常,仍将保留一个值。

  2. 它包含一个值并要删除该值:
    很容易,析构函数不能抛出异常。标准库通常假定用户定义类型不会抛出异常。

  3. 它不包含值并要分配一个值:
    在构造时遇到异常后返回无值的状态就足够了。

  4. 它不包含值且不分配任何值:
    微不足道。

std::variant当存储的类型不变时也同样简单。
然而,当分配不同类型时,它必须通过销毁先前的值为其腾出空间,而构造新值可能会引发异常!

由于先前的值已经丢失,那么你可以怎么做?
将其标记为“因异常而无值”的状态,这样可以有一个稳定的、有效但不理想的状态,并让异常传播。

可能需要使用额外的空间和时间来动态分配值,在某个临时位置保存旧值,在赋值之前构造新值或类似的策略,但所有这些策略都是昂贵的,只有第一种策略始终可行。


5
"valueless by exception"是指需要更改variant中存储的类型的特定情况。这必然需要1)销毁旧值,然后2)在原有位置上创建新值。如果2)失败,则无法回退(这样做会对委员会造成不必要的开销)。 optional没有这个问题。如果它所包含的对象发生异常,那就让它发生吧。对象仍然存在。这并不意味着对象的状态仍然有意义——它取决于抛出异常的操作所处的状态。希望该操作至少具有基本保证。

“*this的初始化状态未改变”…我是否误解了这个声明?我认为你是在说它可能会变成一些没有意义的东西。 - Drew Dormann
2
从“optional”的角度来看,它仍然持有一个对象。该对象是否处于可用状态并不是“optional”所关心的。 - T.C.
std::optional::operator= 使用 T::operator= 而不是销毁 + 构造 T 值,这是一个非常重要的细节。emplace 执行后者(如果新值的构造引发异常,则会使 optional 为空)。 - Max Langhof

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接