When is overloading pass by reference (l-value and r-value) preferred to pass-by-value?
Asked Answered
F

1

29

I have seen it said that a operator= written to take a parameter of the same type by-value serves as both copy assignment operator and move assignment operator in C++11:

Foo& operator=(Foo f)
{
    swap(f);
    return *this;
}

Where the alternative would be more than twice as many lines with a lot of code repetition, and potential for error:

Foo& operator=(const Foo& f)
{
    Foo f2(f);
    swap(f2);
    return *this;
}

Foo& operator=(Foo&& f)
{
    Foo f2(std::move(f));
    swap(f2);
    return *this;
}

In what circumstances is the ref-to-const and r-value overload preferable to pass by value, or when is it necessary? I'm thinking about std::vector::push_back, for example which is defined as two overloads:

void push_back (const value_type& val);
void push_back (value_type&& val);

Following the first example where pass by value serves as copy assignment operator and move assignment operator, couldn't push_back be defined in the Standard to be a single function?

void push_back (value_type val);
Floorage answered 18/8, 2013 at 20:22 Comment(3)
The by-value form can be more expensive (two moves instead of one) when the object is heavy to move (e.g. contains a lot of data members).Contrabass
This might partly be historical reasons, as std::vector already had the T const& overload in C++03. There might be code out there that depends on that overload existing (say someone took the address of the member function). Also note that in the standard there is no need to optimize the lines of code that will have to be implemented, since it is written once by the compiler implementor but use almost everywhere else. The extra cost of development is negligible.Cholula
One case where it is definitely necessary is the copy/move constructor, for obvious reasons.Ambassadress
C
36

For types whose copy assignment operator can recycle resources, swapping with a copy is almost never the best way to implement the copy assignment operator. For example look at std::vector:

This class manages a dynamically sized buffer and maintains both a capacity (maximum length the buffer can hold), and a size (the current length). If the vector copy assignment operator is implemented swap, then no matter what, a new buffer is always allocated if the rhs.size() != 0.

However, if lhs.capacity() >= rhs.size(), no new buffer need be allocated at all. One can simply assign/construct the elements from rhs to lhs. When the element type is trivially copyable, this may boil down to nothing but memcpy. This can be much, much faster than allocating and deallocating a buffer.

Same issue for std::string.

Same issue for MyType when MyType has data members that are std::vector and/or std::string.

There are only 2 times you want to consider implementing copy assignment with swap:

  1. You know that the swap method (including the obligatory copy construction when the rhs is an lvalue) will not be terribly inefficient.

  2. You know that you will always need the copy assignment operator to have the strong exception safety guarantee.

If you're not sure about 2, in other words you think the copy assignment operator might sometimes need the strong exception safety guarantee, don't implement assignment in terms of swap. It is easy for your clients to achieve the same guarantee if you provide one of:

  1. A noexcept swap.
  2. A noexcept move assignment operator.

For example:

template <class T>
T&
strong_assign(T& x, T y)
{
    using std::swap;
    swap(x, y);
    return x;
}

or:

template <class T>
T&
strong_assign(T& x, T y)
{
    x = std::move(y);
    return x;
}

Now there will be some types where implementing copy assignment with swap will make sense. However these types will be the exception, not the rule.

On:

void push_back(const value_type& val);
void push_back(value_type&& val);

Imagine vector<big_legacy_type> where:

class big_legacy_type
{
 public:
      big_legacy_type(const big_legacy_type&);  // expensive
      // no move members ...
};

If we had only:

void push_back(value_type val);

Then push_backing an lvalue big_legacy_type into a vector would require 2 copies instead of 1, even when capacity was sufficient. That would be a disaster, performance wise.

Update

Here is a HelloWorld that you should be able to run on any C++11 conforming platform:

#include <vector>
#include <random>
#include <chrono>
#include <iostream>

class X
{
    std::vector<int> v_;
public:
    explicit X(unsigned s) : v_(s) {}

#if SLOW_DOWN
    X(const X&) = default;
    X(X&&) = default;
    X& operator=(X x)
    {
        v_.swap(x.v_);
        return *this;
    }
#endif
};

std::mt19937_64 eng;
std::uniform_int_distribution<unsigned> size(0, 1000);

std::chrono::high_resolution_clock::duration
test(X& x, const X& y)
{
    auto t0 = std::chrono::high_resolution_clock::now();
    x = y;
    auto t1 = std::chrono::high_resolution_clock::now();
    return t1-t0;
}

int
main()
{
    const int N = 1000000;
    typedef std::chrono::duration<double, std::nano> nano;
    nano ns(0);
    for (int i = 0; i < N; ++i)
    {
        X x1(size(eng));
        X x2(size(eng));
        ns += test(x1, x2);
    }
    ns /= N;
    std::cout << ns.count() << "ns\n";
}

I've coded X's copy assignment operator two ways:

  1. Implicitly, which is equivalent to calling vector's copy assignment operator.
  2. With the copy/swap idiom, suggestively under the macro SLOW_DOWN. I thought about naming it SLEEP_FOR_AWHILE, but this way is actually much worse than sleep statements if you're on a battery powered device.

The test constructs some randomly sized vector<int>s between 0 and 1000, and assigns them a million times. It times each one, sums the times, and then finds the average time in floating point nanoseconds and prints that out. If two consecutive calls to your high resolution clock doesn't return something less than 100 nanoseconds, you may want to raise the length of the vectors.

Here are my results:

$ clang++ -std=c++11 -stdlib=libc++ -O3 test.cpp
$ a.out
428.348ns
$ a.out
438.5ns
$ a.out
431.465ns
$ clang++ -std=c++11 -stdlib=libc++ -O3 -DSLOW_DOWN test.cpp
$ a.out
617.045ns
$ a.out
616.964ns
$ a.out
618.808ns

I'm seeing a 43% performance hit for the copy/swap idiom with this simple test. YMMV.

The above test, on average, has sufficient capacity on the lhs half the time. If we take this to either extreme:

  1. lhs has sufficient capacity all of the time.
  2. lhs has sufficient capacity none of the time.

then the performance advantage of the default copy assignment over the copy/swap idiom varies from about 560% to 0%. The copy/swap idiom is never faster, and can be dramatically slower (for this test).

Want Speed? Measure.

Ciliolate answered 18/8, 2013 at 21:20 Comment(5)
Would copy elision help? Would that remove the other copy in the second case? I'm trying to square with with cpp-next.com/archive/2009/08/want-speed-pass-by-value ThanksFloorage
It doesn't help when the the rhs is an lvalue. Technically there's nothing wrong with that article. However it leaves the wrong impression: Always copy by value is terrible advice. For that matter always "anything" is terrible advice. The article should leave you with the impression that sometimes a by-value parameter can be ok, or even the best thing. But way too many people have overlooked the "sometimes" because the article does not clarify that. Keep pass-by-value in your toolbox. But using it by default, without design, is asking for performance problems.Ciliolate
Note that @HowardHinnant's push_back(const value_type&) example is very hard to write correctly when the argument can be an element, or worse, owned by an element of the vector.Intermix
Does this mean that the copy-and-swap idiom for defining move and copy assignment as a single function is inefficient for any class that contains strings (for example)?Sices
In general, yes. Exceptions would include classes with strings and other members where the other members greatly benefited from copy/swap. This answer is meant to shoot down the notion that copy/swap is always the best way to implement assignment. But I also want to be careful to not say that copy/swap is never a good tool.Ciliolate

© 2022 - 2024 — McMap. All rights reserved.