In this article http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/comment-page-1/#comment-1877 :
T& T::operator=(T const& x) // x is a reference to the source
{
T tmp(x); // copy construction of tmp does the hard work
swap(*this, tmp); // trade our resources for tmp's
return *this; // our (old) resources get destroyed with tmp
}
but in light of copy elision, that formulation is glaringly inefficient! It’s now “obvious” that the correct way to write a copy-and-swap assignment is:
T& operator=(T x) // x is a copy of the source; hard work already done
{
swap(*this, x); // trade our resources for x's
return *this; // our (old) resources get destroyed with x
}
It is said that we should write assignement operator with argument by value rather than by const reference for copy ellision consideration.
With Rvalue reference feature, is it better to write assignement operator like below ? :
T& operator=(T&& x)
{
swap(*this, x);
return *this;
}
there is finally no difference ?
Some types do better with the swap/assign idiom than others. Here's one that doesn't do well:
#include <cstddef>
#include <new>
#include <utility>
template <class T>
class MyVector
{
T* begin_;
T* end_;
T* capacity_;
public:
MyVector()
: begin_(nullptr),
end_(nullptr),
capacity_(nullptr)
{}
~MyVector()
{
clear();
::operator delete(begin_);
}
MyVector(std::size_t N, const T& t)
: MyVector()
{
if (N > 0)
{
begin_ = end_ = static_cast<T*>(::operator new(N*sizeof(T)));
capacity_ = begin_ + N;
for (; N > 0; --N, ++end_)
::new(end_) T(t);
}
}
MyVector(const MyVector& v)
: MyVector()
{
std::size_t N = v.size();
if (N > 0)
{
begin_ = end_ = static_cast<T*>(::operator new(N*sizeof(T)));
capacity_ = begin_ + N;
for (std::size_t i = 0; i < N; ++i, ++end_)
::new(end_) T(v[i]);
}
}
MyVector(MyVector&& v)
: begin_(v.begin_),
end_(v.end_),
capacity_(v.capacity_)
{
v.begin_ = nullptr;
v.end_ = nullptr;
v.capacity_ = nullptr;
}
#ifndef USE_SWAP_ASSIGNMENT
MyVector& operator=(const MyVector& v)
{
if (this != &v)
{
std::size_t N = v.size();
if (capacity() < N)
{
clear();
::operator delete(begin_);
begin_ = end_ = static_cast<T*>(::operator new(N*sizeof(T)));
capacity_ = begin_ + N;
}
std::size_t i = 0;
T* p = begin_;
for (; p < end_ && i < N; ++p, ++i)
(*this)[i] = v[i];
if (i < N)
{
for (; i < N; ++i, ++end_)
::new(end_) T(v[i]);
}
else
{
while (end_ > p)
{
--end_;
end_->~T();
}
}
}
return *this;
}
MyVector& operator=(MyVector&& v)
{
clear();
swap(v);
return *this;
}
#else
MyVector& operator=(MyVector v)
{
swap(v);
return *this;
}
#endif
void clear()
{
while (end_ > begin_)
{
--end_;
end_->~T();
}
}
std::size_t size() const
{return static_cast<std::size_t>(end_ - begin_);}
std::size_t capacity() const
{return static_cast<std::size_t>(capacity_ - begin_);}
const T& operator[](std::size_t i) const
{return begin_[i];}
T& operator[](std::size_t i)
{return begin_[i];}
void swap(MyVector& v)
{
std::swap(begin_, v.begin_);
std::swap(end_, v.end_);
std::swap(capacity_, v.capacity_);
}
};
template <class T>
inline
void
swap(MyVector<T>& x, MyVector<T>& y)
{
x.swap(y);
}
#include <iostream>
#include <string>
#include <chrono>
int main()
{
MyVector<std::string> v1(1000, "1234567890123456789012345678901234567890");
MyVector<std::string> v2(1000, "1234567890123456789012345678901234567890123456789");
typedef std::chrono::high_resolution_clock Clock;
typedef std::chrono::duration<double, std::micro> US;
auto t0 = Clock::now();
v2 = v1;
auto t1 = Clock::now();
std::cout << US(t1-t0).count() << " microseconds\n";
}
Here are results off of my machine:
$ clang++ -std=c++0x -stdlib=libc++ -O3 test.cpp
$ a.out
23.763 microseconds
$ a.out
23.322 microseconds
$ a.out
23.46 microseconds
$ clang++ -std=c++0x -stdlib=libc++ -O3 -DUSE_SWAP_ASSIGNMENT test.cpp
$ a.out
176.452 microseconds
$ a.out
219.474 microseconds
$ a.out
178.15 microseconds
My point: Don't fall into the trap of believing in a silver bullet, or the "one right way to do everything". And the copy/swap idiom is way oversold. It is sometimes appropriate. But in no way is it always appropriate. There is no substitute for careful design, and careful testing.
You want to operate on a copy, otherwise you're removing information from your original object. The idea is to remove information from a temporary copy. It's not highly intuitive but it allows you to use the existing copy constructor and destructor implementations to do the hard work of op=
.
Copy elision is not relevant, because it can't be performed when a copy is semantically required.
Operating on an rvalue reference might be ok, because if you're calling op=
with an rvalue expression as the RHS operand, then it's probably a temporary object and the calling scope probably doesn't want/need to use it any more. However, it's not your op=
's job to assume that.
Your middle approach is canonical.
T& operator=(T x) // x is a copy of the source; hard work already done
{
swap(*this, x); // trade our resources for x's
return *this; // our (old) resources get destroyed with x
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With