Why Is Value Taking Setter Member Functions Not Recommended in Herb Sutter's Cppcon 2014 Talk (Back to Basics: Modern C++ Style)

Why is value taking setter member functions not recommended in Herb Sutter's CppCon 2014 talk (Back to Basics: Modern C++ Style)?

Others have covered the noexcept reasoning above.

Herb spent much more time in the talk on the efficiency aspects. The problem isn't with allocations, its with unnecessary deallocations. When you copy one std::string into another the copy routine will reuse the allocated storage of the destination string if there's enough space to hold the data being copied. When doing a move assignment the destination string's existing storage must be deallocated as it takes over the storage from the source string. The "copy and move" idiom forces the deallocation to always occur, even when you don't pass a temporary. This is the source of the horrible performance that is demonstrated later in the talk. His advice was to instead take a const ref and if you determine that you need it have an overload for r-value references. This will give you the best of both worlds: copy into existing storage for non-temporaries avoiding the deallocation and move for temporaries where you're going to pay for a deallocation one way or the other (either the destination deallocates prior to the move or the source deallocates after the copy).

The above doesn't apply to constructors since there's no storage in the member variable to deallocate. This is nice since constructors often take more than one argument and if you need to do const ref/r-value ref overloads for each argument you end up with a combinatorial explosion of constructor overloads.

The question now becomes: how many classes are there that reuse storage like std::string when copying? I'm guessing that std::vector does, but outside of that I'm not sure. I do know that I've never written a class that reuses storage like this, but I have written a lot of classes that contain strings and vectors. Following Herb's advice won't hurt you for classes that don't reuse storage, you'll be copying at first with the copying version of the sink function and if you determine that the copying is too much of a performance hit you'll then make an r-value reference overload to avoid the copy (just as you would for std::string). On the other hand, using "copy-and-move" does have a demonstrated performance hit for std::string and other types that reuse storage, and those types probably see a lot of use in most peoples code. I'm following Herb's advice for now, but need to think through some of this a bit more before I consider the issue totally settled (there's probably a blog post that I don't have time to write lurking in all this).

Herb Sutter's CppCon Perfect Forwarding slides

The "is not the same as" is a pattern you use when writing constructors that perfectly convert -- you don't want to use this converter when the type passed is some variant of your own type. Odds are it was included here by copy-pasta.

Really, you want to use a trait "this can be assigned to a string": std::enable_if_t<std::is_assignable<std::string, String>::value>>, as that is what you care about. You could go further, and test if it is assignable (if so, use that), and failing that if it is convertible (and if so, convert, then assign), but I wouldn't.

In short, the condition looks like it comes from copy-pasta of a related test. You really don't want to restrict it much.

As for why it beats out option #2, if the std::string in your container already has allocated memory, it can copy from a char const* without allocating more. If instead you take string&&, the char const* is first converted to a string, then that is move-assigned. We have two strings, and one is discarded.

What you are seeing there is memory allocation overhead.

Perfect forwarding doesn't have to allocate memory.


Now, in the interests of being complete, there is another option. It is a bit crazy to implement, but it is nearly as efficient as option #4 and has few of the downsides.

Option 5: type erase assignment. assignment_view<std::string>.

Write a class that type erases "assignment to type T". Take it as your argument. Use it inside.

This is more teachable than perfect forwarding. The method can be virtual, as we are taking a concrete type (the concrete type of assigning to a string). The type erasure happens during the construction of the assigner. Some code is generated for each type assigned-from, but the code is limited to just the assignment, not the entire body of the function.

There is some overhead (similar to a virtual function call, mainly costly due to instruction cache misses) on each assignment. So this isn't perfect.

You call a.assign_to(name) to do the assignment instead of name = a for maximal efficiency. You could do name << std::move(a); if you prefer the syntax.

For maximal efficiency, an assignment erasure view (whatever you want to call it) can only be used to produce one assignment: this allows it to optimize move semantics. You could also make a smart one that does something different on && and & based assign-from for the cost of one extra function pointer overhead.

here I type erase the concept of T == ?. This simply requires type erasing the concept of T = ? instead. (I can make the syntax for {} initialization a tad better with a Ts&&... ctor to the type-erasure object now: that was my first attempt at this.)

live example type erases down to assignment to std::string.

template<class...>struct voider{using type=void;};
template<class...Ts>using void_t=typename voider<Ts...>::type;
template<class T>struct tag{using type=T;};

template<class...>struct types{using type=types;};

template<class T>
using block_deduction = typename tag<T>::type;

template<class F, class Sig, class T=void>
struct erase_view_op;

template<class F, class R, class...Ts, class T>
struct erase_view_op<F, R(Ts...), T>
{
using fptr = R(*)(void const*, Ts&&...);

fptr f;
void const* ptr;

private:
template<class U>
erase_view_op(U&& u, int):
f([](void const* p, Ts&&...ts)->R{
U& u = reinterpret_cast<U&>( *static_cast<std::decay_t<U>*>(const_cast<void*>(p)) );
return F{}( u, std::forward<Ts>(ts)... );
}),
ptr( static_cast<void const*>(std::addressof(u)) )
{}
public:
template<class U, class=std::enable_if_t< !std::is_same<std::decay_t<U>,erase_view_op>{} && (std::is_same<void,R>{} || std::is_convertible< std::result_of_t<F(U,Ts...)>, R >{}) >>
erase_view_op(U&& u):erase_view_op( std::forward<U>(u), 0 ){}

template<class U=T, class=std::enable_if_t< !std::is_same<U, void>{} >>
erase_view_op( block_deduction<U>&& u ):erase_view_op( std::move(u), 0 ){}

erase_view_op( erase_view_op const& ) = default;
erase_view_op( erase_view_op&& ) = default;

R operator()( Ts... ts ) const {
return f( ptr, std::forward<Ts>(ts)... );
}
};

struct assign_lhs_to_rhs {
template<class lhs, class rhs>
void operator()(lhs&& l, rhs& r)const {
r = std::forward<lhs>(l);
}
};
template<class T>
using erase_assignment_to = erase_view_op< assign_lhs_to_rhs, void(T&), T >;
using string_assign_to = erase_assignment_to< std::string >;

it is, as noted, quite similar to type erasing down to ==. I made some modest improvements (void return type). A perfect forwarding (to T{}) ctor would be better than the block_deduction<U>&& one (as you get {} instead of {{}} construction).

Perfect forwarder in Herb Sutter's C++Con 2014 talk

This is to ensure that set_name is only called with an l- or r-value reference to a std::string, allowing for any cv-qualifiers.

It works using SFINAE: if the result of decaying the String template argument is not the same type as std::string, enable_if_t will fail type checking, so that template will be removed from the candidate set.

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.

How to handle invalid state after move especially for objects with validating constructor?

You are trying to maintain two invariants at once, and their semantics are in conflict. The first invariant is the validity of the certificate. The second is for memory management.

For the first invariant, you decided that there can be no invalid constructed object, but for the second, you decided that the object can be either valid or unspecified. This is only possible because the deallocation has a check somewhere.

There is no way around this: you either add a check for the first or you decouple the invariants. One way of decoupling them is to follow the design of std::lock_guard

cert c = open_cert(); // c is guaranteed to not have memory leaks and is movable
{
cert_guard cg{c}; // cg is guaranteed to be valid, but cg is non-movable
}

But wait, you might ask, how do you transfer the validity to another cert_guard?

Well, you can't.

That is the semantics you chose for the first invariant: it is valid exactly during the lifetime of the object. That is the entire point.

† Unspecified and invalid as far as the certificate is concerned.

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