当析构函数可能抛出时,为什么 std::vector 复制构造而不是移动构造?

Why does std::vector copy-construct instead of move-construct when the destructor may throw?

提问人:einpoklum 提问时间:10/10/2022 最后编辑:einpoklum 更新时间:10/15/2022 访问量:1782

问:

请考虑以下程序:

#include <vector>
#include <iostream>

class A {
    int x;
public:
    A(int n)          noexcept : x(n)       { std::cout << "ctor with value\n"; }
    A(const A& other) noexcept : x(other.x) { std::cout << "copy ctor\n"; }
    A(A&& other)      noexcept : x(other.x) { std::cout << "move ctor\n"; }
    ~A()                                    { std::cout << "dtor\n"; } // (*)
};

int main()
{
    std::vector<A> v;
    v.emplace_back(123);
    v.emplace_back(456);
}

如果我运行程序,我会得到 (GodBolt):

ctor with value
ctor with value
move ctor
dtor
dtor
dtor

...这符合我的期望。但是,如果在线我将析构函数标记为可能抛出,那么我会得到(*)

ctor with value
ctor with value
copy ctor
dtor
dtor
dtor

...即使用复制 CTOR 而不是移动 CTOR。为什么会这样?复制似乎并不能防止移动所需的破坏。

相关问题:

C++ stdvector 复制构造函数 move-semantics

评论

0赞 user12002570 10/10/2022
相关/复制:向量重新分配使用复制而不是移动构造函数
2赞 Revolver_Ocelot 10/10/2022
@JasonLiam虽然相关,但绝对不是骗子。这个答案的关键是选择复制构造函数是因为没有标记析构函数。这个问题询问如果析构函数可以抛出异常,为什么选择复制构造函数。noexcept
0赞 user17732522 10/10/2022
链接重复项是关于旧 GCC 版本中的一个错误,该错误与析构函数没有说明符的行为类似这种情况有关。这里的问题是关于说明符的情况。所以我会重新打开。noexceptnoexcept
0赞 davidbak 10/11/2022
O'Dwyer最近的两篇博文是相关的,并且很好读:什么是“向量悲观化”? 和后续 std::vector 的“选二”三角形

答:

21赞 Caleth 10/10/2022 #1

这是LWG2116。移动和复制元素之间的选择通常表示为 ,即 ,这也错误地检查了析构函数。std::is_nothrow_move_constructiblenoexcept(T(T&&))

评论

0赞 einpoklum 10/10/2022
那不应该是这样吗?noexcept(T(T&&)) || !noexcept(T(T))
0赞 Caleth 10/10/2022
@einpoklum不,问题是它根本不应该检查析构函数,因为如果失败,您将无法回到开始时。
1赞 user17732522 10/10/2022
我想这是有道理的。在标准库容器中使用带有潜在抛出析构函数的类型已经是一件不寻常的事情了。他们也不允许从析构函数中实际投掷。
1赞 user17732522 10/11/2022
@benrg 实现不必关心析构函数是否引发,因为这是库用户不会发生这种情况的先决条件。但是,实现必须确保 if 为 false,但类型仍为 CopyInsertable,即从构造函数引发的任何异常都不会导致违反异常保证,这意味着向量必须保持原始状态。这通常无法确保是否在抛出异常之前/期间使用了移动。因此,如果移动可以抛出,则必须使用副本。std::is_nothrow_move_constructible
1赞 Caleth 10/12/2022
@MartinYork移动构造函数是 noexcept,而析构函数不是。对于投掷析构函数来说,选择复制或移动并不重要,因为到那时,你已经结束了某些元素的生存期
13赞 einpoklum 10/11/2022 #2

TL的;dr:因为更愿意为您提供“强异常保证”。std::vector

(感谢 Jonathan Wakely, @davidbak, @Caleth 提供链接和解释)

假设在您的案例中使用移动结构;并假设在向量调整大小期间,其中一个调用将引发异常。在这种情况下,您将有一个 不可用 ,部分移动。std::vectorA::~Astd::vector

另一方面,如果执行复制构造,并且其中一个析构函数中发生异常 - 它可以简单地放弃新副本,并且您的向量将处于调整大小之前的相同状态。这就是对象的“强异常保证”。std::vectorstd::vector

标准库设计人员选择更喜欢这种保证,而不是优化矢量调整大小的性能。

这已被报告为标准库 (LWG 2116) 的问题/缺陷 - 但经过一番讨论,决定根据上述考虑保留当前行为。

另请参阅 Arthur O'Dwyr 的帖子:std::vector 的“选择任意两个”三角形

评论

4赞 ShadowRanger 10/11/2022
Is the strong exception guarantee really relevant here? The destructions (can) all occur after the moves, so the moves have presumably completed and the new contents of the can be locked in (you just get the exception cleaning up the old data). Even if you perform copies, the same exceptions can be raised when you go to clean up the old data you copied from. There's no moral distinction between "copied all the stuff and exception occurred cleaning up old unmodified stuff" and "moved all the stuff and exception occurred cleaning up emptied stuff".vector
1赞 Matthieu M. 10/11/2022
@MartinYork: The question at hand is about destructors throwing, not move or copy constructors. The destructor calls can all be batched after the completion of moving/copying, so the strong exception guarantee seems irrelevant there: the "transaction" has completed by the point the clean-up occurs.
0赞 einpoklum 10/11/2022
@ShadowRanger: That's an interesting point. I suppose that, to the library designers, the destructions are part of the operation. But I see what you mean.
0赞 user17732522 10/15/2022
Having a call to actually throw is not allowed. The standard library containers do not support types that do that. You can also look at implementations. I didn't see the implementations of libstdc++, libc++ or MS guard destructor calls with exception handlers. If a destructor throws an exception the exception is likely to just propagate to the user and leave the vector in an inconsistent state. So this can't really be relevant.A::~A
0赞 user17732522 10/15/2022
The strong exception guarantee is only relevant when a move constructor throws (which is allowed for types used in standard containers). It is just that the description of the guarantee in the standard also depends on the exception specification of the destructor, which doesn't really make sense since the destructor is assumed to not throw anyway.