Is Pass-By-Value a Reasonable Default in C++11

Is pass-by-value a reasonable default in C++11?

It's a reasonable default if you need to make a copy inside the body. This is what Dave Abrahams is advocating:

Guideline: Don’t copy your function arguments. Instead, pass them by value and let the compiler do the copying.

In code this means don't do this:

void foo(T const& t)
{
auto copy = t;
// ...
}

but do this:

void foo(T t)
{
// ...
}

which has the advantage that the caller can use foo like so:

T lval;
foo(lval); // copy from lvalue
foo(T {}); // (potential) move from prvalue
foo(std::move(lval)); // (potential) move from xvalue

and only minimal work is done. You'd need two overloads to do the same with references, void foo(T const&); and void foo(T&&);.

With that in mind, I now wrote my valued constructors as such:

class T {
U u;
V v;
public:
T(U u, V v)
: u(std::move(u))
, v(std::move(v))
{}
};

Otherwise, passing by reference to const still is reasonable.

Is it better in C++ to pass by value or pass by reference-to-const?

It used to be generally recommended best practice1 to use pass by const ref for all types, except for builtin types (char, int, double, etc.), for iterators and for function objects (lambdas, classes deriving from std::*_function).

This was especially true before the existence of move semantics. The reason is simple: if you passed by value, a copy of the object had to be made and, except for very small objects, this is always more expensive than passing a reference.

With C++11, we have gained move semantics. In a nutshell, move semantics permit that, in some cases, an object can be passed “by value” without copying it. In particular, this is the case when the object that you are passing is an rvalue.

In itself, moving an object is still at least as expensive as passing by reference. However, in many cases a function will internally copy an object anyway — i.e. it will take ownership of the argument.2

In these situations we have the following (simplified) trade-off:

  1. We can pass the object by reference, then copy internally.
  2. We can pass the object by value.

“Pass by value” still causes the object to be copied, unless the object is an rvalue. In the case of an rvalue, the object can be moved instead, so that the second case is suddenly no longer “copy, then move” but “move, then (potentially) move again”.

For large objects that implement proper move constructors (such as vectors, strings …), the second case is then vastly more efficient than the first. Therefore, it is recommended to use pass by value if the function takes ownership of the argument, and if the object type supports efficient moving.


A historical note:

In fact, any modern compiler should be able to figure out when passing by value is expensive, and implicitly convert the call to use a const ref if possible.

In theory. In practice, compilers can’t always change this without breaking the function’s binary interface. In some special cases (when the function is inlined) the copy will actually be elided if the compiler can figure out that the original object won’t be changed through the actions in the function.

But in general the compiler can’t determine this, and the advent of move semantics in C++ has made this optimisation much less relevant.


1 E.g. in Scott Meyers, Effective C++.

2 This is especially often true for object constructors, which may take arguments and store them internally to be part of the constructed object’s state.

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.

Where should I prefer pass-by-reference or pass-by-value?

There are four main cases where you should use pass-by-reference over pass-by-value:

  1. If you are calling a function that needs to modify its arguments, use pass-by-reference or pass-by-pointer. Otherwise, you’ll get a copy of the argument.
  2. If you're calling a function that needs to take a large object as a parameter, pass it by const reference to avoid making an unnecessary copy of that object and taking a large efficiency hit.
  3. If you're writing a copy or move constructor which by definition must take a reference, use pass by reference.
  4. If you're writing a function that wants to operate on a polymorphic class, use pass by reference or pass by pointer to avoid slicing.

Best form for constructors? Pass by value or reference?

I've thrown together some examples. I used GCC 4.4.4 in all of this.

Simple case, without -std=c++0x

First, I put together a very simple example with two classes that accept an std::string each.

#include <string>
#include <iostream>

struct A /* construct by reference */
{
std::string s_;

A (std::string const &s) : s_ (s)
{
std::cout << "A::<constructor>" << std::endl;
}
A (A const &a) : s_ (a.s_)
{
std::cout << "A::<copy constructor>" << std::endl;
}
~A ()
{
std::cout << "A::<destructor>" << std::endl;
}
};

struct B /* construct by value */
{
std::string s_;

B (std::string s) : s_ (s)
{
std::cout << "B::<constructor>" << std::endl;
}
B (B const &b) : s_ (b.s_)
{
std::cout << "B::<copy constructor>" << std::endl;
}
~B ()
{
std::cout << "B::<destructor>" << std::endl;
}
};

static A f () { return A ("string"); }
static A f2 () { A a ("string"); a.s_ = "abc"; return a; }
static B g () { return B ("string"); }
static B g2 () { B b ("string"); b.s_ = "abc"; return b; }

int main ()
{
A a (f ());
A a2 (f2 ());
B b (g ());
B b2 (g2 ());

return 0;
}

The output of that program on stdout is as follows:

A::<constructor>
A::<constructor>
B::<constructor>
B::<constructor>
B::<destructor>
B::<destructor>
A::<destructor>
A::<destructor>

Conclusion

GCC was able to optimize each and every temporary A or B away.
This is consistent with the C++ FAQ. Basically, GCC may (and is willing to) generate code that constructs a, a2, b, b2 in place, even if a function is called that appearantly returns by value. Thereby GCC can avoid many of the temporaries whose existence one might have "inferred" by looking at the code.

The next thing we want to see is how often std::string is actually copied in the above example. Let's replace std::string with something we can observe better and see.

Realistic case, without -std=c++0x

#include <string>
#include <iostream>

struct S
{
std::string s_;

S (std::string const &s) : s_ (s)
{
std::cout << " S::<constructor>" << std::endl;
}
S (S const &s) : s_ (s.s_)
{
std::cout << " S::<copy constructor>" << std::endl;
}
~S ()
{
std::cout << " S::<destructor>" << std::endl;
}
};

struct A /* construct by reference */
{
S s_;

A (S const &s) : s_ (s) /* expecting one copy here */
{
std::cout << "A::<constructor>" << std::endl;
}
A (A const &a) : s_ (a.s_)
{
std::cout << "A::<copy constructor>" << std::endl;
}
~A ()
{
std::cout << "A::<destructor>" << std::endl;
}
};

struct B /* construct by value */
{
S s_;

B (S s) : s_ (s) /* expecting two copies here */
{
std::cout << "B::<constructor>" << std::endl;
}
B (B const &b) : s_ (b.s_)
{
std::cout << "B::<copy constructor>" << std::endl;
}
~B ()
{
std::cout << "B::<destructor>" << std::endl;
}
};

/* expecting a total of one copy of S here */
static A f () { S s ("string"); return A (s); }

/* expecting a total of one copy of S here */
static A f2 () { S s ("string"); s.s_ = "abc"; A a (s); a.s_.s_ = "a"; return a; }

/* expecting a total of two copies of S here */
static B g () { S s ("string"); return B (s); }

/* expecting a total of two copies of S here */
static B g2 () { S s ("string"); s.s_ = "abc"; B b (s); b.s_.s_ = "b"; return b; }

int main ()
{
A a (f ());
std::cout << "" << std::endl;
A a2 (f2 ());
std::cout << "" << std::endl;
B b (g ());
std::cout << "" << std::endl;
B b2 (g2 ());
std::cout << "" << std::endl;

return 0;
}

And the output, unfortunately, meets the expectation:

  S::<constructor>
S::<copy constructor>
A::<constructor>
S::<destructor>

S::<constructor>
S::<copy constructor>
A::<constructor>
S::<destructor>

S::<constructor>
S::<copy constructor>
S::<copy constructor>
B::<constructor>
S::<destructor>
S::<destructor>

S::<constructor>
S::<copy constructor>
S::<copy constructor>
B::<constructor>
S::<destructor>
S::<destructor>

B::<destructor>
S::<destructor>
B::<destructor>
S::<destructor>
A::<destructor>
S::<destructor>
A::<destructor>
S::<destructor>

Conclusion

GCC was not able to optimize away the temporary S created by B's constructor. Using the default copy constructor of S did not change that. Changing f, g to be

static A f () { return A (S ("string")); } // still one copy
static B g () { return B (S ("string")); } // reduced to one copy!

did have the indicated effect. It appears that GCC is willing to construct the argument to B's constructor in place but hesitant to construct B's member in place.
Do note that still no temporary A or B are created. That means a, a2, b, b2 are still being constructed in place. Cool.

Let's now investigate how the new move semantics may influence the second example.

Realistic case, with -std=c++0x

Consider adding the following constructor to S

    S (S &&s) : s_ ()
{
std::swap (s_, s.s_);
std::cout << " S::<move constructor>" << std::endl;
}

And changing B's constructor to

    B (S &&s) : s_ (std::move (s)) /* how many copies?? */
{
std::cout << "B::<constructor>" << std::endl;
}

We get this output

  S::<constructor>
S::<copy constructor>
A::<constructor>
S::<destructor>

S::<constructor>
S::<copy constructor>
A::<constructor>
S::<destructor>

S::<constructor>
S::<move constructor>
B::<constructor>
S::<destructor>

S::<constructor>
S::<move constructor>
B::<constructor>
S::<destructor>

B::<destructor>
S::<destructor>
B::<destructor>
S::<destructor>
A::<destructor>
S::<destructor>
A::<destructor>
S::<destructor>

So, we were able to replace four copies with two moves by using pass by rvalue.

But we actually constructed a broken program.

Recall g, g2

static B g ()  { S s ("string"); return B (s); }
static B g2 () { S s ("string"); s.s_ = "abc"; B b (s); /* s is zombie now */ b.s_.s_ = "b"; return b; }

The marked location shows the problem. A move was done on an object that is not a temporary. That's because rvalue references behave like lvalue references except they may also bind to temporaries. So we must not forget to overload B's constructor with one that takes a constant lvalue reference.

    B (S const &s) : s_ (s)
{
std::cout << "B::<constructor2>" << std::endl;
}

You will then notice that both g, g2 cause "constructor2" to be called, since the symbol s in either case is a better fit for a const reference than for an rvalue reference.
We can persuade the compiler to do a move in g in either of two ways:

static B g ()  { return B (S ("string")); }
static B g () { S s ("string"); return B (std::move (s)); }

Conclusions

Do return-by-value. The code will be more readable than "fill a reference I give you" code and faster and maybe even more exception safe.

Consider changing f to

static void f (A &result) { A tmp; /* ... */ result = tmp; } /* or */
static void f (A &result) { /* ... */ result = A (S ("string")); }

That will meet the strong guarantee only if A's assignment provides it. The copy into result cannot be skipped, neither can tmp be constructed in place of result, since result is not being constructed. Thus, it is slower than before, where no copying was necessary. C++0x compilers and move assignment operators would reduce the overhead, but it's still slower than to return-by-value.

Return-by-value provides the strong guarantee more easily. The object is constructed in place. If one part of that fails and other parts have already been constructed, normal unwinding will clean up and, as long as S's constructor fulfills the basic guarantee with regard to its own members and the strong guarantee with regard to global items, the whole return-by-value process actually provides the strong guarantee.

Always pass by value if you're going to copy (onto the stack) anyway

As discussed in Want speed? Pass by value.. The compiler may generate code that constructs, if possible, the caller's argument in place, eliminating the copy, which it cannot do when you take by reference and then copy manually. Principal example:
Do NOT write this (taken from cited article)

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 always prefer this

T& 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
}

If you want to copy to a non-stack frame location pass by const reference pre C++0x and additionally pass by rvalue reference post C++0x

We already saw this. Pass by reference causes less copies to take place when in place construction is impossible than pass by value. And C++0x's move semantics may replace many copies with fewer and cheaper moves. But do keep in mind that moving will make a zombie out of the object that has been moved from. Moving is not copying. Just providing a constructor that accepts rvalue references may break things, as shown above.

If you want to copy to a non-stack frame location and have swap, consider passing by value anyway (pre C++0x)

If you have cheap default construction, that combined with a swap may be more efficient than copying stuff around. Consider S's constructor to be

    S (std::string s) : s_ (/* is this cheap for your std::string? */)
{
s_.swap (s); /* then this may be faster than copying */
std::cout << " S::<constructor>" << std::endl;
}

Example to support the statement pass by value is not good practice even for small user defined types

It depends on whether your copy is deep copy or just shallow copy.(Or value-like class/pointer-like class).For example,A is a class with just one pointer to another object:

struct B;
struct A
{
B* pB;
~A{delete pB;}
}a1,a2;

if you copy A by value,like a1=a2,the default bitwise copy assignment will be called, which is of little cost,however,by doing this you will let the pB in a1,a2 points to the same heap memory.That is to say,the dtor ~A() may be called twice,which is undefined behavior.

So we have to do like this:

   struct A
{
B* pB;
const A& operator=(const A&rhs)
{
if(this!=&rhs)
{
delete pB;
pB=new pB;
*pB=*rhs.pB;
}
return *this;
}
//the copy/move constructor/assignment should also be redefined
~A{delete pB;}
}a1,a2

The code snippet above will call the copy assignment of B,which maybe very costly.

To sum up,if your class is trivially copyable,then copying a small user-defined class,or passing by value, does not cost much,else it depends.

If you still want to pass by value and do not want to trigger undefined behavior,shared_ptr may be a good choice for you.But as pointed out by @Arne Vogel ,the implementation of shared_ptr is thread-safe,which requires atomic operation on reference counting that will increase the cost.



Related Topics



Leave a reply



Submit