34

Consider the following program:

#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); } 

If I run the program, I get (GodBolt):

ctor with value ctor with value move ctor dtor dtor dtor 

... which is in line with what I would expect. However, if on line (*) I mark the destructor as potentially throwing, I then get :

ctor with value ctor with value copy ctor dtor dtor dtor 

... i.e. the copy ctor is used instead of the move ctor. Why is this the case? It doesn't seem copying prevents destructions that moving would necessitate.

Related questions:

4
  • Related/Dupe: Vector reallocation uses copy instead of move constructor. Commented Oct 10, 2022 at 9:37
  • 2
    @JasonLiam while related, definetly not a dupe. Crux of that answer is that copy constructor is chosen because destructor is not marked noexcept. This question asks why copy constructor is chosen if destructor can throw exceptions. Commented Oct 10, 2022 at 9:44
  • Linked duplicate is about a bug in an old GCC version related to the case where the destructor has no noexcept specifier behaves like this. Here the question is about the case with a noexcept specifier. So I'll reopen. Commented Oct 10, 2022 at 9:45
  • Two recent O'Dwyer blog posts are relevant, and good reads: What is the “vector pessimization”? and followup A “pick two” triangle for std::vector. Commented Oct 10, 2022 at 20:19

2 Answers 2

21

This is LWG2116. The choice between moving and copying the elements is often expressed as std::is_nothrow_move_constructible, i.e. noexcept(T(T&&)), which also erroneously checks the destructor.

Sign up to request clarification or add additional context in comments.

11 Comments

Shouldn't that be noexcept(T(T&&)) || !noexcept(T(T)) then?
@einpoklum no, the issue is that it shouldn't be checking the destructor at all, because if that fails you can't go back to what started with.
I guess that makes sense. Using a type with a potentially-throwing destructor in a standard library container is already an unusual thing to do. They don't allow actually throwing from the destructor either.
@benrg The implementation doesn't have to care whether the destructor throws because it is a precondition on the library user that this doesn't happen. The implementation must however assure that if std::is_nothrow_move_constructible is false, but the type still CopyInsertable, that any exception thrown from the constructor(s) won't cause exception guarantees to be violated, meaning the vector must be left in the original state. That is generally impossible to assure if a move was used before/while the exception is thrown. So if the move can throw, then a copy must be used.
@MartinYork the move constructor is noexcept, and the destructor isn't. The choice of copy or move doesn't matter for a throwing destructor, because by that point you have ended the lifetimes of some elements
|
13

tl;dr: Because std::vector prefers to offer you a "strong exception guarantee".

(Thanks goes to Jonathan Wakely, @davidbak, @Caleth for links & explanations)

Suppose std::vector were to use move construction in your case; and suppose that an exception were to be thrown during vector-resizing, by one of the A::~A calls. In that case, you would have an unusable std::vector, partially moved.

On the other hand, if std::vector performs copy construction, and an exception occurs in one of the destructors - it can simply ditch the new copy, and your vector will be in the same state it was before the resizing. That is the "strong exception guarantee" for the std::vector object.

The standard library designers chose to prefer this guarantee over optimizing the performance of vector resizing.

This had been reported as an issue/defect with the standard library (LWG 2116) - but after some discussion, it was decided to keep the current behavior as per the above consideration.

See also Arthur O'Dwyr's post: A "Pick any two" triangle for std::vector.

5 Comments

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 vector 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".
@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.
@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.
Having a call to A::~A 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.
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.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.