2

I have trouble understanding a couple of points regarding the Perfect forwarding option as suggested by Herb Sutter in his presentation "Back to the Basics! Essentials of Modern C++ Style" (@1:15:00) at CppCon 2014. The three relevant slides ( here are the slides online) are the following: enter image description here enter image description here enter image description here

I think that in Option #4 the templated member function should be enabled if the decayed type of Stringis the same as std::stringand not different as stated on the slide (otherwise Options #2 and #4 would not be equivalent and there is no std::stringassignment operator that accepts a non std::string rvalue anyways). But apart from that, I don't undestand

  • what else option #4 can steal from apart from rvalues and
  • why, as shown in the benchmark on the third slide, option #4 would be that much faster than #2 (especially in the last benchmark). Shouldn't they do the same thing?
bmanga
  • 140
  • 1
  • 6
  • `std::string` has an assignment operator that takes a `char const*`. This operator does not have to allocate memory if the `std::string` on the LHS already has sufficient space. – dyp Apr 12 '15 at 15:12
  • @dyp that applies to copying from a `std::string` as well. Also, #4 cannot accept string literals. – bmanga Apr 12 '15 at 15:32
  • 2
    All other options (but perfect forwarding) require the caller to convert to a `std::string` because of the parameter type. If the argument is a `char const*`, this requires a memory allocation, even if the produced `std::string` (function parameter) can be copied without additional allocations to `name_`. As for option #4, IIRC there are some errors on the slide. A perfectly forwarding function would use `std::is_convertible`. – dyp Apr 12 '15 at 15:37
  • This kind of thing is why I feel so strongly that C++ has completely lost its way. It should not be so easy to go so wrong. Before C++11, it wasn't. Almost all new features are hacks. – Lightness Races in Orbit Apr 12 '15 at 15:43
  • @dyp I had doubts for the code as it was but `std::is_convertible` instead of `!is_same` makes a lot more sense to me and would answer my questions. Thank you very much! – bmanga Apr 12 '15 at 15:47
  • 1
    @LightningRacisinObrit: 99% of the C++98/03 features and techniques you know and love are still there for you to use. Just use them and ignore the newer C++11 features if you wish. C++11/14 will still give you some benefits without you changing any syntax of your C++98/03 at all. – Howard Hinnant Apr 12 '15 at 15:57
  • 2
    Related: [What's the correct `enable_if` constraint on perfect forwarding setter?](http://stackoverflow.com/q/26147491/) – dyp Apr 12 '15 at 16:22
  • @HowardHinnant: Yes, I am aware of that. It does not change anything I said! – Lightness Races in Orbit Apr 12 '15 at 16:51

2 Answers2

4

There's an error in the slide, it should be std::enable_if<std::is_same<..., and in fact, there was no error in the slides that were actually shown during the talk, you can see it at 1:16:58:

enter image description here

And yes, as it was pointed out by @dyp, std::enable_if_t<std::is_convertible<String, std::string>::value>> makes much more sense.

Anton Savin
  • 40,838
  • 8
  • 54
  • 90
  • 1
    If the function is only enabled if the function argument is a `std::string`, you won't get benefits for `char const*` arguments. (In fact, you wouldn't be able to pass `char const*`s without additional overloads or manual conversion.) – dyp Apr 12 '15 at 15:40
  • 2
    @dyp: The technique can be tweaked to be a little better with `std::is_assignable` instead. A general guideline for the author is to look at what your implementation is doing, and use the trait(s) that best describe your implementation for your template constraints. – Howard Hinnant Apr 12 '15 at 15:45
  • That's what I thought as well, but as dyp mentioned using `std::is_convertible` instead would better help answer my questions – bmanga Apr 12 '15 at 15:50
  • @HowardHinnant Using `is_assignable` has its own problems - `std::is_assignable::value` is `true`. – T.C. Apr 12 '15 at 22:10
  • @T.C. In which sense is this an issue? Maybe: You can assign a `double` to a `string`, but it is surprising? Or: It is not clear whether or not that should be possible though a mere setter function? – dyp Apr 12 '15 at 22:16
  • @dyp Presumably you don't want `set_name(10.1)` to compile? (well, maybe you think it should, but I'd suggest it shouldn't.) – T.C. Apr 12 '15 at 22:20
  • @T.C. I think this depends on what one thinks about `std::string s; s = 10.1;` ;) -- but it's not obvious to me what the restriction should be, if it shall not be *perfectly* (and stupidly) forwarding. – dyp Apr 12 '15 at 23:19
2

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

Community
  • 1
  • 1
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Well, I also completely forgot about that trait until Howard Hinnant mentioned it ;) -- although I'm not entirely sure if it's the appropriate trait here. `is_convertible` mimics the restrictions of a function with a `std::string const&` parameter. As T.C. noted, you can assign a `double` to a `string`, but you cannot convert a `double` to a `string`. – dyp Apr 13 '15 at 16:21
  • @dyp I think that is a flaw in `string` more than anything else. Anyhow, added that type erasure implementation. You can now use `erase_assignment_to` or `string_assign_to` as a static parameter. Pretty silly if your code is a single line thing, but might be easier to use than manual perfect forwarding for a new user. – Yakk - Adam Nevraumont Apr 13 '15 at 16:34