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