When Is Overloading Pass by Reference (L-Value and R-Value) Preferred to Pass-By-Value

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

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.

Pass by value vs pass by rvalue reference

The rvalue reference parameter forces you to be explicit about copies.

Yes, pass-by-rvalue-reference got a point.

The rvalue reference parameter means that you may move the argument, but does not mandate it.

Yes, pass-by-value got a point.

But that also gives to pass-by-rvalue the opportunity to handle exception guarantee: if foo throws, widget value is not necessary consumed.

For move-only types (as std::unique_ptr), pass-by-value seems to be the norm (mostly for your second point, and first point is not applicable anyway).

EDIT: standard library contradicts my previous sentence, one of shared_ptr's constructor takes std::unique_ptr<T, D>&&.

For types which have both copy/move (as std::shared_ptr), we have the choice of the coherency with previous types or force to be explicit on copy.

Unless you want to guarantee there is no unwanted copy, I would use pass-by-value for coherency.

Unless you want guaranteed and/or immediate sink, I would use pass-by-rvalue.

For existing code base, I would keep consistency.

Pass by value or rvalue-ref

The pass-by-value function is sufficient (and equivalent), as long as the argument type has an efficient move constructor, which is true in this case for std::vector.

Otherwise, using the pass-by-value function may introduce an extra copy-construction compared to using the pass-by-rvalue-ref function.

See the answer https://stackoverflow.com/a/7587151/1190077 to the related question Do I need to overload methods accepting const lvalue reference for rvalue references explicitly? .

Advantages of pass-by-value and std::move over pass-by-reference

  1. Did I understand correctly what is happening here?

Yes.


  1. Is there any upside of using std::move over passing by reference and just calling m_name{name}?

An easy to grasp function signature without any additional overloads. The signature immediately reveals that the argument will be copied - this saves callers from wondering whether a const std::string& reference might be stored as a data member, possibly becoming a dangling reference later on. And there is no need to overload on std::string&& name and const std::string& arguments to avoid unnecessary copies when rvalues are passed to the function. Passing an lvalue

std::string nameString("Alex");
Creature c(nameString);

to the function that takes its argument by value causes one copy and one move construction. Passing an rvalue to the same function

std::string nameString("Alex");
Creature c(std::move(nameString));

causes two move constructions. In contrast, when the function parameter is const std::string&, there will always be a copy, even when passing an rvalue argument. This is clearly an advantage as long as the argument type is cheap to move-construct (this is the case for std::string).

But there is a downside to consider: the reasoning doesn't work for functions that assign the function argument to another variable (instead of initializing it):

void setName(std::string name)
{
m_name = std::move(name);
}

will cause a deallocation of the resource that m_name refers to before it's reassigned. I recommend reading Item 41 in Effective Modern C++ and also this question.

C++11: Why rvalue reference parameter implicitly converted to lvalue

One, the x argument to fn isn't an r-value reference, it's a "universal reference" (yes, this is rather confusing).

Two, the moment you give an object a name, that name is not an r-value unless explicitly "fixed", either with std::move (to make it an r-value reference, always), or with std::forward (to convert it back to its original type in the case of universal references). If you want to avoid the complaint, use std::forward to forward as the original type:

template <class T>
void fn (T&& x) {
overloaded(std::forward<T>(x));
}

Overload resolution between object, rvalue reference, const reference

What are the rules here?

As there is only one parameter, the rule is that one of the three viable parameter initializations of that parameter must be a better match than both the other two. When two initializations are compared, either one is better than the other, or neither is better (they are indistinguishable).

Without special rules about direct reference binding, all three initializations mentioned would be indistinguishable (in all three comparisons).

The special rules about direct reference binding make int&& better than const int&, but neither is better or worse than int. Therefore there is no best match:

S1    S2
int int&& indistinguishable
int const int& indistinguishable
int&& const int& S1 better

int&& is better than const int& because of 13.3.3.2:

S1 and S2 are reference bindings (8.5.3) and neither refers to an implicit object parameter of a non-static member function declared without a ref-qualifier, and S1 binds an rvalue reference to an rvalue and S2 binds an lvalue reference.

But this rule does not apply when one of the initializations is not a reference binding.

Is there any chance int && may be preferred over int in a future standard? The reference must bind to an initializer, whereas the object type is not so constrained. So overloading between T and T && could effectively mean "use the existing object if I've been given ownership, otherwise make a copy."

You propose to make a reference binding a better match than a non-reference binding. Why not post your idea to isocpp future proposals. SO is not the best for subjective discussion / opinion.

Why is passing by value (if a copy is needed) recommended in C++11 if a const reference only costs a single copy as well?

When consuming data, you'll need an object you can consume. When you get a std::string const& you will have to copy the object independent on whether the argument will be needed.

When the object is passed by value the object will be copied if it has to be copied, i.e., when the object passed is not a temporary. However, if it happens to be a temporary the object may be constructed in place, i.e., any copies may have been elided and you just pay for a move construction. That is, there is a chance that no copy actually happens.



Related Topics



Leave a reply



Submit