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:

einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • Related/Dupe: [Vector reallocation uses copy instead of move constructor](https://stackoverflow.com/questions/18655588/vector-reallocation-uses-copy-instead-of-move-constructor). – Jason Oct 10 '22 at 09: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. – Revolver_Ocelot Oct 10 '22 at 09: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. – user17732522 Oct 10 '22 at 09:45
  • Two recent O'Dwyer blog posts are relevant, and good reads: [_What is the “vector pessimization”?_](https://quuxplusone.github.io/blog/2022/08/26/vector-pessimization/) and followup [_A “pick two” triangle for `std::vector`_](https://quuxplusone.github.io/blog/2022/09/30/vector-pessimization-pick-two/). – davidbak Oct 10 '22 at 20:19

2 Answers2

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.

Caleth
  • 52,200
  • 2
  • 44
  • 75
  • Shouldn't that be `noexcept(T(T&&)) || !noexcept(T(T))` then? – einpoklum Oct 10 '22 at 09:33
  • @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. – Caleth Oct 10 '22 at 09:40
  • However I don't see anything in the standard that requires the `std::vector` implementation to actually use `std::is_nothrow_move_constructible` to decide on copy/move. The exception-safety guarantees are specified in terms of it, but a weaker check only on the constructor would still satisfy them, I think. (https://www.eel.is/c++draft/containers#vector.modifiers-2) – user17732522 Oct 10 '22 at 09:54
  • @user17732522 the point is that some implementations do use `is_nothrow_move_constructible`, or something with an equivalently overstrong check – Caleth Oct 10 '22 at 09:57
  • 1
    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. – user17732522 Oct 10 '22 at 10:03
  • @user17732522: That still doesn't explain why copying is preferred over moving (as opposed to the type being rejected altogether). – einpoklum Oct 10 '22 at 14:03
  • @einpoklum Rejecting is not allowed. `std::vector` must accept types that are MoveInsertible and may use copy construction if that is possible. Whether or not the move construction throws is irrelevant to that. Neither does it matter whether the destructor is potentially-throwing. The precondition is just that the destructor may not _actually_ throw, which the compiler can't verify. – user17732522 Oct 10 '22 at 14:08
  • @user17732522 I guess einpoklum's confusion (or mine at least) is why any implementation would prefer copying over moving given that you have to call the destructor either way. (And a potentially-throwing destructor is if anything more likely to actually throw if you use the copy constructor.) – benrg Oct 10 '22 at 17:39
  • 1
    @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. – user17732522 Oct 10 '22 at 17:52
  • @benrg Then it seems the implementation(s) choose to just use the mentioned type trait (which probably is itself defect for considering the destructor) from the exception-safety guarantee's description directly, not optimizing for this very unsual case of a potentially-throwing, but not actually throwing, destructor on a class with a `noexcept` move constructor. – user17732522 Oct 10 '22 at 17:53
  • 1
    @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 – Caleth Oct 11 '22 at 20:14
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.

einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • 4
    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". – ShadowRanger Oct 11 '22 at 00:52
  • 1
    @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. – Matthieu M. Oct 11 '22 at 07:33
  • @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. – einpoklum Oct 11 '22 at 13:25
  • 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. – user17732522 Oct 14 '22 at 23:47
  • 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. – user17732522 Oct 14 '22 at 23:49