Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

vector::push_back insists on using copy constructor though a move constructor is provided

I was receiving a strange error from gcc and cannot figure out why. I made the following example code to make the problem more clear. Basically, there is a class defined, for which I make its copy constructor and copy assignment operator private, to prevent calling them accidentally.

#include <vector>
#include <cstdio>
using std::vector;

class branch 
{
public:
  int th;

private:
  branch( const branch& other );
  const branch& operator=( const branch& other );

public:

  branch() : th(0) {}

  branch( branch&& other )
  {
    printf( "called! other.th=%d\n", other.th );
  }

  const branch& operator=( branch&& other )
  {
    printf( "called! other.th=%d\n", other.th );
    return (*this);
  }

};



int main()
{
  vector<branch> v;
  branch a;
  v.push_back( std::move(a) );

  return 0;
}

I expect this code to compile, but it fails with gcc. Actually gcc complains that "branch::branch(const branch&) is private", which as I understand shouldn't be called.

The assignment operator works, since if I replace the body of main() with

branch a;
branch b;
b = a;

It will compile and run as expected.

Is this a correct behavior of gcc? If so, what's wrong with the above code? Any suggestion is helpful to me. Thank you!

like image 821
BreakDS Avatar asked Jul 15 '12 00:07

BreakDS


People also ask

Does std::vector Push_back make a copy?

Yes, std::vector<T>::push_back() creates a copy of the argument and stores it in the vector.

Does vector have a move constructor?

No. It doesn't call the move constructor. To call move constructor of element you will have to call std::move while pushing to vector itself.

What happens if you use the default copy constructor for vector?

The default copy constructor will copy all members – i.e. call their respective copy constructors. So yes, a std::vector (being nothing special as far as C++ is concerned) will be duly copied.


2 Answers

Try adding "noexcept" to the declaration of the move constructor.

I can't quote the standard, but recent versions of gcc appear to require either that the copy constructor be public or that the move constructor be declared "noexcept". Regardless of the "noexcept" qualifier, if you make the copy constructor public, it will behave as you expect at run-time.

like image 177
mawicks Avatar answered Nov 16 '22 10:11

mawicks


Unlike suggested by the previous answer, gcc 4.7 was wrong to reject this code, a mistake which has been corrected in gcc 4.8.

The full standard-conforming behavior for vector<T>::push_back is:

  • If there is only a copy constructor and no move constructor, push_back will copy its argument and will give the strong exception safety guarantee. That is, if the push_back fails due to an exception triggered by reallocation of the vector storage, the original vector will remain unchanged and usable. This is the known behavior from C++98 and it is also the reason for the mess that follows.
  • If there is a noexcept move constructor for T, push_back will move from its argument and will give the strong exception guarantee. No surprises here.
  • If there is a move constructor that is not noexcept and there is also a copy constructor, push_back will copy the object and give the strong exception safety guarantee. This is unexpected at first glance. While push_back could move here, that would only be possible at the expense of sacrificing the strong exception guarantee. If you ported code from C++98 to C++11 and your type is movable, that would silently change the behavior of existing push_back calls. To avoid this pitfall and maintain compatibility with C++98 code, C++11 falls back to the slower copy. This is what the gcc 4.7 behavior is all about. But there is more...
  • If there is a move constructor that is not noexcept but no copy constructor at all - that is, the element can only be moved and not copied - push_back will perform the move but will not give the strong exception safety guarantee. This is where gcc 4.7 went wrong. In C++98 there are no push_backs for types that are movable but not copyable. So sacrificing the strong exception safety here does not break existing code. This is why it is allowed and the original code is in fact legal C++11.

See cppreference.com on push_back:

If an exception is thrown, this function has no effect (strong exception guarantee).

If T's move constructor is not noexcept and the copy constructor is not accessible, vector will use the throwing move constructor. If it throws, the guarantee is waived and the effects are unspecified.

Or a little more convoluted §23.3.6.5 from the C++11 Standard (emphasis added by me):

Causes reallocation if the new size is greater than the old capacity. If no reallocation happens, all the iterators and references before the insertion point remain valid. If an exception is thrown other than by the copy constructor, move constructor, assignment operator, or move assignment operator of T or by any InputIterator operation there are no effects. If an exception is thrown by the move constructor of a non-CopyInsertable T, the effects are unspecified.

Or if you don't like reading, Scott Meyer's Going Native 2013 talk (starting at 0:30:20 with the interesting part at about 0:42:00).

like image 35
ComicSansMS Avatar answered Nov 16 '22 10:11

ComicSansMS