C++移动语义和异常

27
在即将发布的C++0x标准中,如果在移动构造函数期间抛出异常会发生什么?
原始对象会保留吗?还是原始对象和移动到的对象都处于未定义状态?语言提供了哪些保证?

我在想这是否像析构函数一样,如果它抛出异常就是一件坏事。我认为在大多数情况下,你可以移动指针而不必执行任何可能会抛出异常的操作,因为你通常只是在移动指针... - templatetypedef
3
那么你的意思是,在移动操作期间被复制的成员的状态从未改变,因此不应该有任何可能会引发异常的方式? - Matthieu N.
2
Sorta-它类似于你用归纳论证明swap不能抛出异常的论证。作为基本情况,重新分配和复制原语永远不会抛出异常。作为归纳步骤,具有一些数据成员的对象的移动构造函数可以在不抛出异常的情况下移动这些数据成员(归纳假设!),因此可以在不抛出异常的情况下移动自身。然而,独立地,我仍然想知道您问题的正确答案是什么。 - templatetypedef
3个回答

12

我相信标准委员会最初尝试使移动构造函数不能抛出异常,但至少从今天的情况来看,他们发现试图强制执行这一点有太多的缺陷。

提案N3050,“允许移动构造函数抛出异常(修订1版)”已纳入草案标准。本质上,该提案增加了移动构造函数抛出异常的能力,但禁止“抛出式移动”用于某些需要强烈的异常安全保证的操作(如果没有非抛出式移动可用,则库将回退到复制对象)。

如果将移动构造函数标记为不抛出 (noexcept) 且抛出异常,将调用 std::terminate()。

阅读David Abrahams的博客文章也可能很值得,该文章讨论了N3050旨在解决的问题:


感谢David Abrahams的文章 :) - Matthieu M.
3
是的,我也发现整个 cpp-next 博客网站非常有趣,有很多有价值的文章! - Matthieu N.

3
根据移动来源的类型而定。当然,从移动构造函数中明确抛出异常是可能的,但也可以从移动构造函数隐式调用子对象的复制构造函数。该复制构造函数可能会执行一些可能抛出异常的操作,例如分配内存。因此,对于源对象,最小保证是原始值可能仍然存在,但应仍处于可销毁状态。
对于被移动对象,与在当前C ++中从构造函数中抛出相同:销毁任何已构造的基类和成员,执行构造函数的函数尝试处理程序(如果有),然后传播异常。详细信息请参见N3225 §15.2p2。
特别要注意的是,容器要求其分配器类型不具有可抛出的移动构造函数:
这种分配器的移动构造不得通过异常退出。[N3225 §23.2p8]
这允许容器移动分配器并使用这些分配器在移动或复制项目时清理其项目中的条目。

3
你的问题涉及到异常保证。函数有三种类型的异常保证:
- 根本没有异常保证(不是真正的一种类型...但如果不关注此事,可能会发生) - 基本异常保证:技术上是正确的,但不是功能上正确的(即没有资源泄漏,程序将在没有突然停止的情况下终止,但可能会产生意外的副作用,例如支付已经被兑现,但命令未被注册) - 强异常保证:全有或全无(像交易一样),要么一切都做得对,要么我们回滚到之前的状态。 - 不抛出异常保证:这永远不会抛出异常,所以不必担心。
当你组合函数时,通常会选择具有自己保证的现有函数。增加异常保证很困难,因为通常限于使用的最弱保证。
关于你的问题,原始对象至少需要一个强异常保证才能在抛出异常时不被改变。
那么,如果在移动构造过程中抛出异常会发生什么?这取决于子对象展示的保证以及您组合调用的方式...
- 如果从构造函数抛出异常,则对象未被构建,所有已构建的子对象按相反顺序销毁。该规则也适用于移动构造函数。 - 除非您在try catch中“包装”构造函数并以某种方式恢复已移动的对象,否则它们将失去其资源。请注意,它们仍必须处于可销毁状态,因此从技术上讲,程序将是正确的。
就异常保证而言,默认情况下,如果所有子对象的构造函数至少满足基本异常保证,则您的移动构造函数也会满足此要求,无需特殊处理。
但是,即使所有子对象的构造函数都满足强异常保证,您也很难成功使自己的移动构造函数满足它:这与链接事务不产生事务的问题相同。
如果只有一个子对象的构造函数可能引发异常,并且它满足强异常保证,则如果首先初始化抛出对象,则您的移动构造函数自然会满足它本身的要求。
希望这有所帮助...异常是难以驯服的野兽 :)

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