Passing by value vs const & and && overloads
Asked Answered
S

3

41

So after looking up move semantics I see that general consensus is to pass by value when you intend to transfer ownership. But in Scott Meyer's talk on Universal references I've noticed that std::vector::push_back has 2 overloads:

void push_back( const T& value );
void push_back( T&& value );

So I thought to myself, wouldn't void push_back( T value ); be enough? I've asked a few people which ultimately lead to the following test case:

#include <memory>
#include <iostream>
#include <type_traits>

struct A
{
    A() { std::cout << "A Default constructor\n"; }
    A(const A &) { std::cout << "A Copy\n"; }
    A(A &&) { std::cout << "A Move\n"; }
};

std::aligned_storage<sizeof(A)> contents;
A& alias = *reinterpret_cast<A*>(&contents);

void ByVal(A a)
{
    new (&contents) A(std::move(a));
    alias.~A();
}

void ByLCRef(A const& a)
{
    new (&contents) A(a);
    alias.~A();
}

void ByRRef(A&& a)
{
    new (&contents) A(std::move(a));
    alias.~A();
}

int main()
{
    A a;
    std::cout << "\n";
    std::cout << "ByVal(a);\n";
    ByVal(a);
    std::cout << "ByVal(std::move(a));\n";
    ByVal(std::move(a));
    std::cout << "ByVal(A());\n";
    ByVal(A());
    std::cout << "ByLCRef(a);\n";
    ByLCRef(a);
    std::cout << "ByRRef(std::move(a));\n";
    ByRRef(std::move(a));
    std::cout << "ByRRef(A());\n";
    ByRRef(A());
}

Which produces the following:

A Default constructor

ByVal(a);
A Copy
A Move
ByVal(std::move(a));
A Move
A Move
ByVal(A());
A Default constructor
A Move
ByLCRef(a);
A Copy
ByRRef(std::move(a));
A Move
ByRRef(A());
A Default constructor
A Move

As you can see, ByVal produces 1 extra move compared to pair of reference overloads. So the question is: is it worth it? When would you create two overloads instead of one simple pass by value function?

Stuccowork answered 6/1, 2013 at 19:42 Comment(8)
With std::vector there isn't much choice, as it already had a pass by const reference signature. That can't be removed. For your own new classes, you have more options.Gilolo
@BoPersson: But many old functions has been replaced with new ones with new signatures! Why not std::vector also?Cockayne
I would argue that no code would be broken if the signature would be changed. I think it all comes to the extra move.Stuccowork
If you have an existing user defined type that is copyable but not movable, you will get a different result than with the class A above. Like two copies instead of one.Gilolo
@BoPersson: For example see these insert overloads : insert : see until vs since remarksCockayne
@Nawaz - Those changes are not visible, as C++11 also requires an iterator to be convertible to a const_iterator, so it will select the same overload.Gilolo
Were you using placement-new to somehow avoid optimization? Just curious.Adrianneadriano
@0x499602D2: No, that wasn't my idea. But so far the results are consistent with Howard's.Stuccowork
J
24

As you can see, ByVal produces 1 extra move compared to pair of reference overloads. So the question is: is it worth it? When would you create two overloads instead of one simple pass by value function?

+1 Most people who ask this question don't bother to do the analysis. So you get my upvote for doing your own homework. :-)

Whether it is worth it or not is going to depend on the cost of the move constructor, and on how many arguments the function takes. On one extreme, if the move constructor isn't that fast, you may care a lot about eliminating them (favoring the const&, && overload solution). At the other extreme, if your function has 4 parameters, each of which need lvalue/rvalue treatment, you may not be willing to write 16 overloads to cover all the cases. That's a lot of code to maintain, and the inherent code complexity is an invitation for bugs. So the by-value approach looks more attractive (which requires no overloads).

So imho, there is no general answer to the "is it worth it" question. The best answer is to equip yourself with the knowledge about the cost of each solution, as you have already done, and make an engineering judgement on a case by case basis.

Update

In the case of vector<T>::push_back imho the const&, && overload solution is worth it. There is only one parameter, and we have no idea how expensive the move constructor is. Indeed, we don't even know if there is a move constructor. Modifying your experiment to test out that latter case (removing the move constructor):

ByVal(a);
A Copy
A Copy

ByLCRef(a);
A Copy

Do you want to pay one copy or two to copy your A into the vector?

I.e. the less you know about your parameters, the more you have to lean towards the performance side, especially if you're writing something as heavily used as std::vector.

Joh answered 6/1, 2013 at 21:7 Comment(0)
B
6

Storing a moveable and copyable class

Imagine you have this class:

class Data {
 public:
  Data() { }
  Data(const Data& data)            { std::cout << "  copy constructor\n";} 
  Data(Data&& data)                 { std::cout << "  move constructor\n";}
  Data& operator=(const Data& data) { std::cout << "  copy assignment\n"; return *this;}
  Data& operator=(Data&& data)      { std::cout << "  move assignment\n"; return *this;}  
};

Note, a good C++11 compiler should define all these functions for you (some old versions of Visual Studio don't) but I'm defining them here for debug output.

Now, if you wanted to write a class to store one of these classes I might use pass-by-value like you suggest:

class DataStore {
  Data data_;
 public: 
  void setData(Data data) { data_ = std::move(data); }
};

I am taking advantage of C++11 move semantics to move the value to the desired location. I can then use this DataStore like this:

  Data d;   
  DataStore ds;
  
  std::cout << "DataStore test:\n";
  ds.setData(d);
  
  std::cout << "DataStore test with rvalue:\n";
  ds.setData(Data{});
  
  Data d2;
  std::cout << "DataStore test with move:\n";
  ds.setData(std::move(d2));

Which has the following output:

DataStore test:
  copy constructor
  move assignment
DataStore test with rvalue:
  move assignment
DataStore test with move:
  move constructor
  move assignment

Which is fine. I have two moves in the last test which might not be optimum but moves are typically cheap so I can live with that. To make it more optimum we would need to overload the setData function which we will do later but that's probably premature optimization at this point.

Storing an unnmovable class

But now imagine we have a copyable but unmovable class:

class UnmovableData {
 public:
  UnmovableData() { }
  UnmovableData(const UnmovableData& data) { std::cout << "  copy constructor\n";}
  UnmovableData& operator=(const UnmovableData& data) { std::cout << "  copy assignment\n"; return *this;}  
};

Before C++11, all classes were unmovable so expect to find lots of them in the wild today. If I needed to write a class to store this I can't take advantage of move semantics so I would probably write something like this:

class UnmovableDataStore {
  UnmovableData data_;
 public:
  void setData(const UnmovableData& data) { data_ = data; }
};

and pass by reference-to-const. When I use it:

  std::cout << "UnmovableDataStore test:\n";
  UnmovableData umd;
  UnmovableDataStore umds;
  umds.setData(umd);

I get the output:

UnmovableDataStore test:
  copy assignment

with only one copy as you would expect.

Storing an uncopyable class

You could also have a movable but noncopyable class:

class UncopyableData {
 public:
  UncopyableData() { } 
  UncopyableData(UncopyableData&& data) { std::cout << "  move constructor\n";}
  UncopyableData& operator=(UncopyableData&& data) { std::cout << "  move assignment\n"; return *this;}    
};

std::unique_ptr is an example of a movable but noncopyable class. In this case I would probably write a class to store it like this:

class UncopyableDataStore {
  UncopyableData data_;
 public:
  void setData(UncopyableData&& data) { data_ = std::move(data); }
};

where I pass by rvalue reference and use it like this:

  std::cout << "UncopyableDataStore test:\n";
  UncopyableData ucd;
  UncopyableDataStore ucds;
  ucds.setData(std::move(ucd));

with the following output:

UncopyableDataStore test:
  move assignment

and notice we now only have one move which is good.

Generic containers

The STL containers however need to be generic, they need to work with all types of classes and be as optimal as possible. And if you really needed a generic implementation of the data stores above it might look like this:

template<class D>
class GenericDataStore {
  D data_;
 public:
  void setData(const D& data) { data_ = data; }
  void setData(D&& data) { data_ = std::move(data); }   
};

In this way we get the best possible performance whether we are using uncopyable or unmovable classes but we have to have at least two overloads of the setData method which might introduce duplicate code. Usage:

  std::cout << "GenericDataStore<Data> test:\n";
  Data d3;
  GenericDataStore<Data> gds;
  gds.setData(d3);
  
  std::cout << "GenericDataStore<UnmovableData> test:\n";
  UnmovableData umd2;
  GenericDataStore<UnmovableData> gds3;
  gds3.setData(umd2); 
  
  std::cout << "GenericDataStore<UncopyableData> test:\n";
  UncopyableData ucd2;
  GenericDataStore<UncopyableData> gds2;
  gds2.setData(std::move(ucd2));

Output:

GenericDataStore<Data> test:
  copy assignment
GenericDataStore<UnmovableData> test:
  copy assignment
GenericDataStore<UncopyableData> test:
  move assignment

Live demo. Hope that helps.

Bosket answered 22/5, 2014 at 13:38 Comment(0)
S
5

One important point is that client code does not need to be changed when switching between passing by value and overloading. So it really comes down to performance-vs-maintenance. And since maintenance is usually favored more, I've come up with the following rule of thumb:

Pass by value unless:
1. Move constructor or move assignment is not trivial.
2. The object is copyable but non-movable.
3. You are writing a template library and do not know the type of the object.
4. Despite object having trivial move constructor and assignment, your profiler still shows you that program spends a lot of time inside moves.

Stuccowork answered 7/1, 2013 at 2:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.