Is the Pass-By-Value-And-Then-Move Construct a Bad Idiom

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.

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&.

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.

Is it better to pass by value for moveable types instead of overloading functions?

The first version is usually marginally faster but it introduces code duplication. The second way is recommended for objects that are cheap to move. Since most objects are cheap to move in C++, the second way is usually fine. But, there is one more alternative:

template<typename T>
void SetParams(T&& params) { mParams = std::forward<T>(params); }

template<typename T>
void SetBoardInfo(T&& board_info) { mBoardInfo = std::forward<T>(board_info); }

Thanks to universal references, this is the best of both worlds if it is OK to have templated setters.

move or copy when passing arguments to the constructor and member functions

  • If it is a primitive type, pass by value. Locality of reference wins.

  • If you aren't going to store a copy of it, pass by value or const&.

  • If you want to store a copy of it, and it is very cheap to move and modestly expensive to copy, pass by value.

  • If something has a modest cost to move, and is a sink parameter, consider pass by rvalue reference. Users will be forced to std::move.

  • Consider providing a way for callers to emplace construct into the field in highly generic code, or where you need every ounce of performance

The Rule of 0/3/5 describes how you should handle copy assign/construct/destroy. Ideally you follow the rule of 0; copy/move/destruct is all =default in anything except resource management types. If you want to implement any of copy/move/destruct, you need to implement, =default or =delete every other one of the 5.

If you are only taking 1 argument to a setter, consider writing both the && and const& versions of the setter. Or just exposing the underlying object. Move-assignment sometimes reuses storage and that is efficient.

Emplacing looks like this:

struct emplace_tag {};
struct wrap_foo {
template<class...Ts>
wrap_foo(emplace_tag, Ts&&...ts):
foo( std::forward<Ts>(ts)... )
{}
template<class T0, class...Ts>
wrap_foo(emplace_tag, std::initializer_list<T0> il, Ts&&...ts):
foo( il, std::forward<Ts>(ts)... )
{}
private:
Foo foo;
};

there are a myriad of other ways you can permit "emplace" construction. See emplace_back or emplace in standard containers as well (where they use placement ::new to construct objects, forwarding objects passed in).

Emplace construct even permits direct construction without even a move using objects with an operator T() setup properly. But that is something that is beyond the scope of this question.



Related Topics



Leave a reply



Submit