Advantages of Pass-By-Value and Std::Move Over Pass-By-Reference

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.

Pass-by-value and std::move vs forwarding reference

But it seems to me that passing by either const reference or rvalue reference can save a copy in some situations.

Indeed, but it requires more overloads (and even worst with several parameters).

Pass by value and move idiom has (at worst) one extra move. which is a good trade-off most of the time.

maybe using a forwarding reference to avoid writing both constructors.

Forwarding reference has its own pitfalls:

  • disallows {..} syntax for parameter as {..} has no type.
    Test2 a({5u, '*'}); // "*****"
    would not be possible.
  • is not restrict to valid types (requires extra requires or SFINAE).
    Test2 b(4.2f); // Invalid, but `std::is_constructible_v<Test2, float>` is (falsely) true.
    would produces error inside the constructor, and not at call site (so error message less clear, and SFINAE not possible).
  • for constructor, it can take precedence over copy constructor (for non-const l-value)
    Test2 c(a); // Call Test2(T&&) with T=Test2&
    // instead of copy constructor Test2(const Test2&)
    would produce error, as std::string cannot be constructed from Test2&.

IDE recommends 'pass by value and use std move' when parameter class has no move constructor

Your Person class is following the "rule-of-zero", meaning it doesn't declare any copy/move operations or destructor explicitly.

That is usually the correct thing to do, because then the compiler will declare all of them implicitly and define them with the semantics you would usually expect from the these operations, if that is possible.

In your case, all members of Person can be move-constructed, so the implicit move constructor of Person will be defined to move-construct the members one-by-one.

This is what is used when you add std::move in mData(std::move(data)). Without it data is a lvalue and instead the implicit copy constructor which copy-constructs each member would be used.

Copy-constructing a std::string is often more costly than move-constructing it, which is probably why the tool warns about that case, but not if std::string isn't there. Move-constructing and copy-constructing an int member is exactly the same operation.

If you don't want to check each time what members a class has, you can simply always use std::move in these situations. For example changing mId(id) to mId(std::move(id)) is pointless, because id is a scalar type, but it also doesn't make anything worse.

Is the pass-by-value-and-then-move construct a bad idiom?

Expensive-to-move types are rare in modern C++ usage. If you are concerned about the cost of the move, write both overloads:

void set_a(const A& a) { _a = a; }
void set_a(A&& a) { _a = std::move(a); }

or a perfect-forwarding setter:

template <typename T>
void set_a(T&& a) { _a = std::forward<T>(a); }

that will accept lvalues, rvalues, and anything else implicitly convertible to decltype(_a) without requiring extra copies or moves.

Despite requiring an extra move when setting from an lvalue, the idiom is not bad since (a) the vast majority of types provide constant-time moves and (b) copy-and-swap provides exception safety and near-optimal performance in a single line of code.

Passing a parameter to a function with std::move, is there a difference if that parameter is declared as pass-by-value or with a move operand &&?

ValueClass requires the move constructor of std::string to be called twice: Once to create the obj parameter and then to initialize the m_obj member.

MoveClass requires only one move constructor of std::string to be called: obj is an (rvalue) reference to b (no new std::string object is constructed here). Only the m_obj initialization involves the std::string move constructor.


There is still the question whether it would be allowed to elide any of these calls (as an exception to the as-if rule). That is not the case though; neither member initialization nor function parameter initialization are a valid context for eliding constructors (or temporaries): https://en.cppreference.com/w/cpp/language/copy_elision


Now, in the given code, there are not necessarily side effects that require the compiler to keep any of the constructor calls. In fact, if you don't perform the reassignment in main, gcc notices that the entire program has no side effect and can be optimized to a no-op.

https://godbolt.org/z/hxrVfC

You also didn't give MSVC the correct parameters. Highest optimization is /O2, not -O3 (although -O2 does work). Mind the compiler warnings!

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.

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/reference for a parameter that will be moved within function

If your function will definitely move from the given value, then the best way to express this is by taking an rvalue-reference parameter (T&&). This way, if the user tries to pass an lvalue directly, they will get a compile error. And that forces them to invoke std::move directly on the lvalue, which visibly indicates to the reader that the value will be moved-from.

Using an lvalue-reference is always wrong. Lvalue-references don't bind to xvalues and prvalues (ie: expressions where it's OK to move from them), so you're kind of lying with such an interface.



Related Topics



Leave a reply



Submit