11

Given class X below (special member functions other than the one explicitly defined are not relevant for this experiment):

struct X
{
    X() { }
    X(int) { }
    X(X const&) { std::cout << "X(X const&)" << std::endl; }
    X(X&&) { std::cout << "X(X&&)" << std::endl; }
};

The following program creates a vector of objects of type X and resizes it so that its capacity is exceeded and reallocation is forced:

#include <iostream>
#include <vector>

int main()
{
    std::vector<X> v(5);
    v.resize(v.capacity() + 1);
}

Since class X provides a move constructor, I would expect the previous content of the vector to be moved into the new storage after reallocation. Quite surprisingly, that does not seem to be the case, and the output I get is:

X(X const&)
X(X const&)
X(X const&)
X(X const&)
X(X const&)

Why?

Andy Prowl
  • 124,023
  • 23
  • 387
  • 451
  • Also note that _Microsoft_ doesn't respect this rule so this code compiled with _Visual C++_ (or _clang_ on _Windows_) will call move constructor and thus may leave you with broken (empty) elements in `std::vector` in case of resize failure. – Denis Jul 06 '17 at 11:38

2 Answers2

21

Paragraph 23.3.6.3/14 of the C++11 Standard specifies (about the resize() member function of the vector<> class template):

Remarks: If an exception is thrown other than by the move constructor of a non-CopyInsertable T there are no effects.

In other words, this means that for X (which is CopyInsertable), resize() offers the strong guarantee: it either succeeds or leaves the state of the vector unchanged.

In order to satisfy this guarantee, implementations usally adopt the copy-and-swap idiom: if the copy constructor of X throws, we haven't altered the content of the original vector yet, so the promise is kept.

However, if the previous content of the vector were moved into the new storage rather than being copied and the move constructor threw, then we would have irreversibly changed the original content of the vector.

Therefore, implementations will use the copy constructor of X to safely transfer the content of the vector into a new storage unless the move constructor is known not to throw, in which case it is safe to move from the previous elements.

With a small change to the definition of X's move constructor (marking it as noexcept), in fact, the output of the program is now the expected one.:

struct X
{
    X() { }
    X(int) { }
    X(X const&) { std::cout << "X(X const&)" << std::endl; }
    X(X&&) noexcept { std::cout << "X(X&&)" << std::endl; }
//         ^^^^^^^^
};
Community
  • 1
  • 1
Andy Prowl
  • 124,023
  • 23
  • 387
  • 451
  • 1
    What's happening -- a blog post question? :-) – Kerrek SB Mar 31 '13 at 15:28
  • 1
    @KerrekSB: I've been told self-answered questions are OK, I thought this one would be interesting :) – Andy Prowl Mar 31 '13 at 15:28
  • @AndyProwl - I saw someone else try to do this and they were bludgeoned... – chue x Mar 31 '13 at 15:31
  • 2
    Well, they are, but usually only if you have a genuine problem and later figure it out yourself. If you already know the answer ahead of time, it's a bit fishy. You could mark the question as "FAQ" and "Community Wiki", I suppose, if you just want to share some information. – Kerrek SB Mar 31 '13 at 15:31
  • 4
    @KerrekSB: But that's what happened. I had a problem, I figured out what was going on, and I thought I'd share it. See, for instance, [this](http://stackoverflow.com/questions/15568959/is-a-declaration-valid-inside-an-if-block-with-no-actual-block). I thought it was OK. – Andy Prowl Mar 31 '13 at 15:35
  • @Andy - Perhaps you haven't been here long enough to see the long and winding answers tagged [c++-faq]. There are some very long and extremely thorough answers to questions nobody ever asked before. I have added that tag to my ignore list. :-) – Bo Persson Mar 31 '13 at 15:56
  • @BoPersson: I did not claim this is worth being a faq, I just found a problem amd answered it, and thought I'd share it. I didn't realize it was a dupe, otherwise I wouldn't have posted it. I did the same before, following the example of others, and nobody complained. Now I know it is not a good idea – Andy Prowl Mar 31 '13 at 16:22
  • 1
    @Andy - It might be a good idea, but some people might also see this as you trying to make up a question because you have a nice answer (which is what happened earlier). If nobody has ever asked about this before, it just *could* be (unlikely in this case) that everyone else knew the answer. I did. :-) – Bo Persson Mar 31 '13 at 16:28
  • 5
    Answering your own question, whether or not the answer was already known, is [explicitly encouraged](http://blog.stackoverflow.com/2011/07/its-ok-to-ask-and-answer-your-own-questions/). – rhashimoto Apr 07 '13 at 12:39
  • 3
    +1 Please keep answering your own questions. I learn a lot from your answers. – Olaf Dietsche Apr 11 '13 at 06:24
  • 2
    @OlafDietsche: OK, thank you for your support. I'm glad someone found it helpful. – Andy Prowl Apr 11 '13 at 08:57
5

Think about the exception guarantees: If there's an exception during the reallocation, the vector has to remain unchanged. This can only be guaranteed by copying the elements and retaining the old set until the entire copy has succeeded.

Only if you know that the move constructor doesn't throw can you safely move the elements to the new location. To achieve this, declare the move constructor noexcept.

Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084