Why does std::vector have two assignment operators?
Asked Answered
N

2

17

Since 2011, we have both copy and move assignment. However, this answer argues quite convincingly that, for resource managing classes, one needs only one assignment operator. For std::vector, for example, this would look like

vector& vector::operator=(vector other)
{
  swap(other);
  return*this;
}

The important point here is that the argument is taken by value. This means that at the moment the function body proper is entered, much of the work has already been done by the construction of other (by move constructor if possible, otherwise by copy constructor). Hence, this automatically implements both copy and move assignment correctly.

If this is correct, why is (according to this documentation at least) std::vector not implemented in this way?


edit to explain how this works. Consider what happens to other in above code in the following examples

void foo(std::vector<bar> &&x)
{
  auto y=x;             // other is copy constructed
  auto z=std::move(x);  // other is move constructed, no copy is ever made.
  // ...
}
Nicolas answered 20/11, 2015 at 23:1 Comment(10)
I've also wondered about this. Copy-and-swap seems quite awesome. Maybe it's because of legacy reasons, needing to keep the function signature exactly the same? I'm curious to see a good answer.Charwoman
This requires a memory allocation. Copying the contents from an lvalue may not, if the assignee has enough capacity.Centenarian
@Centenarian I also thought that. If this is so, then the nice answer by GManNickG is not as nice at it locks?Nicolas
@Centenarian If you implement the move constructor (and swap) properly, it doesn't really require any memory allocation, or at least no more than a move-assignment operator.Charwoman
@Charwoman Yes, it does. The argument is a copy. If it is an lvalue, then you really do cause an allocation which may not be necessary. OTOH, if std::vector assignment provides the strong exception guarantee I think an allocation is needed anyway.Centenarian
@Charwoman I think juanchopanza was talking about the copy assignment.Nicolas
See Howard Hinnant's answer to this question.Centenarian
@Centenarian Unfortunately, Howard's answer is not specific to this problem, since the case considered doesn't manage resources, but merely a std::string (which itself manages resources).Nicolas
@Nicolas I think (but am not 100% sure) that the standard waives the strong exception guarantee for move assignment of std::vectors. If that is the case, then the extra assignment involved in copy and swap can be avoided. But I have to go and check if this is really the case.Centenarian
There's also the "fun with allocators" part. With stateful, non-propagating allocators, there's no guarantee that you can just make a copy of the rhs and swap it into place. (And when you can't, the copy becomes a complete waste.)Deficiency
H
7

If the element type is nothrow copyable, or the container does not honor the strong exception guarantee, then a copy-assignment operator can avoid allocation in the case where the destination object has sufficient capacity:

vector& operator=(vector const& src)
{
    clear();
    reserve(src.size());  // no allocation if capacity() >= src.size()
    uninitialized_copy_n(src.data(), src.size(), dst.data());
    m_size = src.size();
}
Hydrogen answered 20/11, 2015 at 23:39 Comment(1)
Okay, that's a fair argument (and was already covered in the comments). It seems indeed that the C++ standard makes no exception guarantee for the copy assignment, but from C++17 possibly for the move assignment (allocator allowing).Nicolas
D
-2

Actually there are three assignment operators defined:

vector& operator=( const vector& other );
vector& operator=( vector&& other );
vector& operator=( std::initializer_list<T> ilist );

Your suggestion vector& vector::operator=(vector other) uses the copy and swap idiom. That means, that when the operator is called, the original vector will be copied into the parameter, copying every single item in the vector. Then this copy will be swapped with this. The compiler might be able to elide that copy, but that copy elision is optional, move semantics is standard.

You can use that idiom to replace the copying assignment operator:

vector& operator=( const vector& other ) {
    swap(vector{other}); // create temporary copy and swap
    return *this;
}

Whenever copying any of the elements throws, this function will throw as well.

To implement the moving assignment operator just leave out the copying:

vector& operator=( vector&& other ) {
    swap(other);
    return *this;
}

Since swap() does never throws, neither will the move assignment operator.

The initializer_list-assignment can also be easily implemented by using the move assignment operator and an anonymous temporary:

vector& operator=( std::initializer_list<T> ilist ) {
    return *this = vector{ilist};
}

We used the move assignment operator. As a concequence the initializer_list assignment operatpr will only throw, when one of the element instantiations throws.

As I said, the compiler might be able to elide the copy for the copy assignment. But the compiler is not obliged to implement that optimization. It is obliged to implement move semantics.

Dibbell answered 20/11, 2015 at 23:28 Comment(8)
Did you actually read the answer referenced in my post? The point is that your two implementations of the move and copy assignment are perfectly and identially implemented by the one implementation in my question.Nicolas
@Nicolas that is only true, when the compiler does implement copy elision. The standard provides all freedom to the compiler vendor to do so, but it does not require it.Dibbell
So you're saying that when a function taking an argument by value is called with an rvalue reference, it's legal to no move but copy instead (to initialise the function argument)?Nicolas
The code states, that the compiler should use the copy constructor here. In that case the standard allows the compiler to elide the copy, when it would chose a move instead. You are relying on an optimization, that most current compiler do implement. That is great for your code, but not for interfaces contained in the standard. You should rely on copy elision, wherever it works, but the standard can not, since it does not require the compiler to implement copy elision.Dibbell
The code states, that the compiler should use the copy constructor here Does it? Can you give a quote from the standard that this is so? In a comparable situation of bar x=std::move(y), no copy is requested.Nicolas
@Nicolas OK, lets dig deeper. I'll have to split the answer in three for length reasons. Your example bar x = std::move(y) is an initialization. With that syntax, the compiler has to check, weather a potentially fitting assignment operator exists, so that the syntax is legal, but will simply use the move constructor.Dibbell
That is different to x = std::move(y). In that case the compiler has to use the assignment operator, because it is not an initialization. std::move() will return an rvalue reference to the given object. Then the compiler will chose a fitting assignment operator. In your case that is operator = (vector other). This means it has a rvalue reference to an object, which it will have to copy to the parameter object.Dibbell
Here copy elision kicks in and the compiler is allowed to skip the copy my simply using the space on the stack for the parameter as target for a move. So if the compiler supports copy elision, it will exactly create the code, you described, but if it doesn't it will create code that copies the vector.Dibbell

© 2022 - 2024 — McMap. All rights reserved.