为什么 std::variant 需要在移动分配中变为 valueless_by_exception?
Why is std::variant required to become valueless_by_exception in move assignment?
我看到了以下关于 valueless_by_exception
方法的 cppreference 注释:
A variant may become valueless in the following situations:
- (guaranteed) an exception is thrown during the move initialization of the contained value during move assignment
- (optionally) an exception is thrown during the copy initialization of the contained value during copy assignment
所以,这样的代码
std::variant<MyClass> var = {...};
var = myClassObj;
不需要使 var.valueless_by_exception()
等于 true(并且大概会使 var
保持其先前的状态),但是此代码
std::variant<MyClass> var = {...};
var = std::move(myClassObj);
如果发生异常,保证使var.valueless_by_exception()
等于真。
复制和移动分配之间的规范差异的实际原因是什么?
请注意,Cppreference 指的是不同的情况。当它说 copy/move 赋值时,它是在谈论 copy/move 赋值 来自变体 ,而不是来自 T
。来自 T
的赋值在下一条语句中处理:
(optionally) an exception is thrown when initializing the contained value during a type-changing assignment
变体的移动分配失败总是会使目标变体毫无价值的原因是没有其他选择。
这里手头的问题是特定于作业的。当您将一个 variant
分配给另一个时,有两种可能性。这两个变体要么具有相同的 Ti
类型,要么不具有。
如果它们共享相同的 Ti
,那么它们存储的类型之间的 copy/move 赋值可以直接发生。当发生这种情况时,variant 为正在使用的 Ti
提供赋值操作的异常保证。变体本身绝不会变得毫无价值;它总是有一个活动的 Ti
,它处于失败的 copy/move 赋值运算符留下的任何状态。
但是,如果两个变体在 Ti
上不同,则目标变体必须销毁其当前对象并通过 copy/move [=] 创建源类型 Ti
的新对象 构造(不是赋值)。
如果 variant 提供强异常保证,这意味着如果此 copy/move 构造失败,则 variant 仍应保留赋值运算符之前的对象类型和值。但是您会注意到该对象在创建新对象之前被销毁了。这是因为 variant
存储了其所有 Ts
的联合,因此它们都共享内存。您不能在不先破坏那里的任何东西的情况下尝试在变体中创建 Ti
。
一旦被摧毁,它就消失了,无法恢复。
所以现在我们有一个不包含原始类型的变体,我们未能在其中创建 Ti
。那么...它包含什么?
没有。
从副本中变为无值是可选的原因是,如果一个类型抛出复制构造而不是移动构造(许多 throwing-copy 类型提供),则可以实现复制操作有两个对象的解决方案。在 销毁内部变体对象之前,您对堆栈临时 执行 copy-initialization。如果成功,您可以从堆栈对象中销毁内部对象和 move-construct。
委员会不想要求这个实现,但它也不想禁止它。
我看到了以下关于 valueless_by_exception
方法的 cppreference 注释:
A variant may become valueless in the following situations:
- (guaranteed) an exception is thrown during the move initialization of the contained value during move assignment
- (optionally) an exception is thrown during the copy initialization of the contained value during copy assignment
所以,这样的代码
std::variant<MyClass> var = {...};
var = myClassObj;
不需要使 var.valueless_by_exception()
等于 true(并且大概会使 var
保持其先前的状态),但是此代码
std::variant<MyClass> var = {...};
var = std::move(myClassObj);
如果发生异常,保证使var.valueless_by_exception()
等于真。
复制和移动分配之间的规范差异的实际原因是什么?
请注意,Cppreference 指的是不同的情况。当它说 copy/move 赋值时,它是在谈论 copy/move 赋值 来自变体 ,而不是来自 T
。来自 T
的赋值在下一条语句中处理:
(optionally) an exception is thrown when initializing the contained value during a type-changing assignment
变体的移动分配失败总是会使目标变体毫无价值的原因是没有其他选择。
这里手头的问题是特定于作业的。当您将一个 variant
分配给另一个时,有两种可能性。这两个变体要么具有相同的 Ti
类型,要么不具有。
如果它们共享相同的 Ti
,那么它们存储的类型之间的 copy/move 赋值可以直接发生。当发生这种情况时,variant 为正在使用的 Ti
提供赋值操作的异常保证。变体本身绝不会变得毫无价值;它总是有一个活动的 Ti
,它处于失败的 copy/move 赋值运算符留下的任何状态。
但是,如果两个变体在 Ti
上不同,则目标变体必须销毁其当前对象并通过 copy/move [=] 创建源类型 Ti
的新对象 构造(不是赋值)。
如果 variant 提供强异常保证,这意味着如果此 copy/move 构造失败,则 variant 仍应保留赋值运算符之前的对象类型和值。但是您会注意到该对象在创建新对象之前被销毁了。这是因为 variant
存储了其所有 Ts
的联合,因此它们都共享内存。您不能在不先破坏那里的任何东西的情况下尝试在变体中创建 Ti
。
一旦被摧毁,它就消失了,无法恢复。
所以现在我们有一个不包含原始类型的变体,我们未能在其中创建 Ti
。那么...它包含什么?
没有。
从副本中变为无值是可选的原因是,如果一个类型抛出复制构造而不是移动构造(许多 throwing-copy 类型提供),则可以实现复制操作有两个对象的解决方案。在 销毁内部变体对象之前,您对堆栈临时 执行 copy-initialization。如果成功,您可以从堆栈对象中销毁内部对象和 move-construct。
委员会不想要求这个实现,但它也不想禁止它。