提问人:einpoklum 提问时间:10/10/2022 最后编辑:einpoklum 更新时间:10/15/2022 访问量:1782
当析构函数可能抛出时,为什么 std::vector 复制构造而不是移动构造?
Why does std::vector copy-construct instead of move-construct when the destructor may throw?
问:
请考虑以下程序:
#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。为什么会这样?复制似乎并不能防止移动所需的破坏。
相关问题:
答:
21赞
Caleth
10/10/2022
#1
这是LWG2116。移动和复制元素之间的选择通常表示为 ,即 ,这也错误地检查了析构函数。std::is_nothrow_move_constructible
noexcept(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::vector
A::~A
std::vector
另一方面,如果执行复制构造,并且其中一个析构函数中发生异常 - 它可以简单地放弃新副本,并且您的向量将处于调整大小之前的相同状态。这就是对象的“强异常保证”。std::vector
std::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.
评论
noexcept
noexcept
noexcept
std::vector
的“选二”三角形。