4

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 ?

Andriy Tylychko
  • 15,967
  • 6
  • 64
  • 112
Guillaume Paris
  • 10,303
  • 14
  • 70
  • 145

3 Answers3

10

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.

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • 1
    I'd be happier with your results if each test was 1-5 seconds. Your margin of error is kind high – Mooing Duck Jan 13 '12 at 16:09
  • The precision of `high_resolution_clock` on this platform is a few nanoseconds. I'm showing tens of thousands of nanoseconds, even up to hundreds of thousands of nanoseconds. And I'm showing the test results 3 times for each case to give the reader a feel for the margin of error. One case is 7.7 times faster than the other. If the truth is that one case is running only 6 times faster than the other, or even only twice as fast as the other, or even only 20% faster than the other, my point stands. – Howard Hinnant Jan 13 '12 at 16:32
  • 1
    oh, yeah. Point stands definitely. I was just thinking about statistically... nevermind, it doesnt matter. – Mooing Duck Jan 13 '12 at 17:03
  • 1
    Can you summarise the _reason_ for the performance issue? My head's not really up to the analysis today :) – Lightness Races in Orbit Jan 13 '12 at 17:04
  • In this test case the copy assignment operator (under `#ifndef USE_SWAP_ASSIGNMENT`) does not go to the heap. The one using the copy/swap idiom does. Going to the heap is expensive. A good copy assignment operator will reuse resources such as heap memory when possible. – Howard Hinnant Jan 13 '12 at 17:13
2

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
}
Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
1

With Rvalue reference feature, is it better to write assignement operator like below ?

It is not better, since these two operators are different (see rule of five).

The 1st (T& T::operator=(T const& x)) is for assigning l-values, while the 2nd (T& operator=(T&& x)) is for r-values. Take a note that this would fail to compile if you had only the 2nd implemented :

#include <iostream>

struct T
{
  T(int v):a(v){}

  T& operator=( const T& t)
  {
    std::cout<<"copy"<<std::endl;
    a=t.a;
    return *this;
  }
  T& operator=( T&& t)
  {
    std::cout<<"move"<<std::endl;
    a=std::move(t.a);
    return *this;
  }

  int a;
};

void foo( const T &t)
{
  T tmp(2);
  std::cout<<tmp.a<<std::endl;
  tmp=t;
  std::cout<<tmp.a<<std::endl;
}
void bar(T &&t)
{
  T tmp(2);
  std::cout<<tmp.a<<std::endl;
  tmp=std::move(t);
  std::cout<<tmp.a<<std::endl;
}

int main( void )
{
  T t1(1);
  std::cout<<"foo"<<std::endl;
  foo(t1);
  std::cout<<"bar"<<std::endl;
  bar(T(5));
}
Community
  • 1
  • 1
BЈовић
  • 62,405
  • 41
  • 173
  • 273