When is a const reference better than pass-by-value in C++11?

I have some pre-C++11 code in which I use const references to pass large parameters like vector's a lot. An example is as follows:

int hd(const vector<int>& a) {
   return a[0];
}

I heard that with new C++11 features, you can pass the vector by value as follows without performance hits.

int hd(vector<int> a) {
   return a[0];
}

For example, this answer says

C++11's move semantics make passing and returning by value much more attractive even for complex objects.

Is it true that the above two options are the same performance-wise?

If so, when is using const reference as in option 1 better than option 2? (i.e. why do we still need to use const references in C++11).

One reason I ask is that const references complicate deduction of template parameters, and it would be a lot easier to use pass-by-value only, if it is the same with const reference performance-wise.


Solution 1:

The general rule of thumb for passing by value is when you would end up making a copy anyway. That is to say that rather than doing this:

void f(const std::vector<int>& x) {
    std::vector<int> y(x);
    // stuff
}

where you first pass a const-ref and then copy it, you should do this instead:

void f(std::vector<int> x) {
    // work with x instead
}

This has been partially true in C++03, and has become more useful with move semantics, as the copy may be replaced by a move in the pass-by-val case when the function is called with an rvalue.

Otherwise, when all you want to do is read the data, passing by const reference is still the preferred, efficient way.

Solution 2:

There is a big difference. You will get a copy of a vector's internal array unless it was about to die.

int hd(vector<int> a) {
   //...
}
hd(func_returning_vector()); // internal array is "stolen" (move constructor is called)
vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8};
hd(v); // internal array is copied (copy constructor is called)

C++11 and the introduction of rvalue references changed the rules about returning objects like vectors - now you can do that (without worrying about a guaranteed copy). No basic rules about taking them as argument changed, though - you should still take them by const reference unless you actually need a real copy - take by value then.

Solution 3:

C++11's move semantics make passing and returning by value much more attractive even for complex objects.

The sample you give, however, is a sample of pass by value

int hd(vector<int> a) {

So C++11 has no impact on this.

Even if you had correctly declared 'hd' to take an rvalue

int hd(vector<int>&& a) {

it may be cheaper than pass-by-value but performing a successful move (as opposed to a simple std::move which may have no effect at all) may be more expensive than a simple pass-by-reference. A new vector<int> must be constructed and it must take ownership of the contents of a. We don't have the old overhead of having to allocate a new array of elements and copy the values over, but we still need to transfer the data fields of vector.

More importantly, in the case of a successful move, a would be destroyed in this process:

std::vector<int> x;
x.push(1);
int n = hd(std::move(x));
std::cout << x.size() << '\n'; // not what it used to be

Consider the following full example:

struct Str {
    char* m_ptr;
    Str() : m_ptr(nullptr) {}
    Str(const char* ptr) : m_ptr(strdup(ptr)) {}
    Str(const Str& rhs) : m_ptr(strdup(rhs.m_ptr)) {}
    Str(Str&& rhs) {
      if (&rhs != this) {
        m_ptr = rhs.m_ptr;
        rhs.m_ptr = nullptr;
      }
    }
    ~Str() {
      if (m_ptr) {
        printf("dtor: freeing %p\n", m_ptr)
        free(m_ptr);
        m_ptr = nullptr;
      }
    }
};

void hd(Str&& str) {
  printf("str.m_ptr = %p\n", str.m_ptr);
}

int main() {
  Str a("hello world"); // duplicates 'hello world'.
  Str b(a); // creates another copy
  hd(std::move(b)); // transfers authority for b to function hd.
  //hd(b); // compile error
  printf("after hd, b.m_ptr = %p\n", b.m_ptr); // it's been moved.
}

As a general rule:

  • Pass by value for trivial objects,
  • Pass by value if the destination needs a mutable copy,
  • Pass by value if you always need to make a copy,
  • Pass by const reference for non-trivial objects where the viewer only needs to see the content/state but doesn't need it to be modifiable,
  • Move when the destination needs a mutable copy of a temporary/constructed value (e.g. std::move(std::string("a") + std::string("b"))).
  • Move when you require locality of the object state but want to retain existing values/data and release the current holder.

Solution 4:

Remember that if you are not passing in an r-value, then passing by value would result in a full blown copy. So generally speaking, passing by value could lead to a performance hit.

Solution 5:

Your example is flawed. C++11 does not give you a move with the code that you have, and a copy would be made.

However, you can get a move by declaring the function to take an rvalue reference, and then passing one:

int hd(vector<int>&& a) {
   return a[0];
}

// ...
std::vector<int> a = ...
int x = hd(std::move(a));

That's assuming that you won't be using the variable a in your function again except to destroy it or to assign to it a new value. Here, std::move casts the value to an rvalue reference, allowing the move.

Const references allow temporaries to be silently created. You can pass in something that is appropriate for an implicit constructor, and a temporary will be created. The classic example is a char array being converted to const std::string& but with std::vector, a std::initializer_list can be converted.

So:

int hd(const std::vector<int>&); // Declaration of const reference function
int x = hd({1,2,3,4});

And of course, you can move the temporary in as well:

int hd(std::vector<int>&&); // Declaration of rvalue reference function
int x = hd({1,2,3,4});