Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Constructor using std::forward

To my knowledge, the two common ways of efficiently implementing a constructor in C++11 are using two of them

Foo(const Bar& bar) : bar_{bar} {};
Foo(Bar&& bar)      : bar_{std::move(bar)} {};

or just one in the fashion of

Foo(Bar bar) : bar_{std::move(bar)} {};

with the first option resulting in optimal performance (e.g. hopefully a single copy in case of an lvalue and a single move in case of an rvalue), but needing 2N overloads for N variables, whereas the second option only needs one function at the cost of an additional move when passing in an lvalue.

This shouldn't make too much of an impact in most cases, but surely neither choice is optimal. However one could also do the following:

template<typename T>
Foo(T&& bar) : bar_{std::forward<T>(bar)} {};

This has the disadvantage of allowing variables of possibly unwanted types as the bar parameter (which is a problem I'm sure is easily resolved using template specialization), but in any case performance is optimal and the code grows linearly with the amount of variables.

Why is nobody using something like forward for this purpose? Isn't it the most optimal way?

like image 282
grinsekrokodil Avatar asked Sep 22 '16 18:09

grinsekrokodil


People also ask

What is std :: forward used for?

The idiomatic use of std::forward is inside a templated function with an argument declared as a forwarding reference , where the argument is now lvalue , used to retrieve the original value category, that it was called with, and pass it on further down the call chain (perfect forwarding).

What is forwarding reference in C++?

When t is a forwarding reference (a function argument that is declared as an rvalue reference to a cv-unqualified function template parameter), this overload forwards the argument to another function with the value category it had when passed to the calling function.

What do std :: move and std :: forward do?

std::move takes an object and casts it as an rvalue reference, which indicates that resources can be "stolen" from this object. std::forward has a single use-case: to cast a templated function parameter of type forwarding reference ( T&& ) to the value category ( lvalue or rvalue ) the caller used to pass it.

What is C++ perfect forwarding?

What is Perfect Forwarding. Perfect forwarding allows a template function that accepts a set of arguments to forward these arguments to another function whilst retaining the lvalue or rvalue nature of the original function arguments.


1 Answers

People do perfect forward constructors.

There are costs.

First, the cost is that they must be in the header file. Second, each use tends to result in a different constructor being created. Third, you cannot use {}-like initialization syntax for the objects you are constructing from.

Fourth, it interacts poorly with the Foo(Foo const&) and Foo(Foo&&) constructors. It will not replace them (due to language rules), but it will be selected over them for Foo(Foo&). This can be fixed with a bit of boilerplate SFINAE:

template<class T,
  std::enable_if_t<!std::is_same<std::decay_t<T>, Foo>{},int> =0
>
Foo(T&& bar) : bar_{std::forward<T>(bar)} {};

which now is no longer preferred over Foo(Foo const&) for arguments of type Foo&. While we are at it we can do:

Bar bar_;
template<class T,
  std::enable_if_t<!std::is_same<std::decay_t<T>, Foo>{},int> =0,
  std::enable_if_t<std::is_constructible<Bar, T>{},int> =0
>
Foo(T&& bar) :
  bar_{std::forward<T>(bar)}
{};

and now this constructor only works if the argument can be used to construct bar.

The next thing you'll want to do is to either support {} style construction of the bar, or piecewise construction, or varargs construction where you forward into bar.

Here is a varargs variant:

Bar bar_;
template<class T0, class...Ts,
  std::enable_if_t<sizeof...(Ts)||!std::is_same<std::decay_t<T0>, Foo>{},int> =0,
  std::enable_if_t<std::is_constructible<Bar, T0, Ts...>{},int> =0
>
Foo(T0&&t0, Ts&&...ts) :
  bar_{std::forward<T0>(t0), std::forward<Ts>(ts)...}
{};
Foo()=default;

On the other hand, if we add:

Foo(Bar&& bin):bar_(std::move(bin));

we now support Foo( {construct_bar_here} ) syntax, which is nice. However this isn't required if we already have the above varardic (or a similar piecewise construct). Still, sometimes an initializer list is nice to forward, especially if we don't know the type of bar_ when we write the code (generics, say):

template<class T0, class...Ts,
  std::enable_if_t<std::is_constructible<Bar, std::initializer_list<T0>, Ts...>{},int> =0
>
Foo(std::initializer_list<T0> t0, Ts&&...ts) :
  bar_{t0, std::forward<Ts>(ts)...}
{};

so if Bar is a std::vector<int> we can do Foo( {1,2,3} ) and end up with {1,2,3} within bar_.

At this point, you gotta wonder "why didn't I just write Foo(Bar)". Is it really that expensive to move a Bar?

In generic library-esque code, you'll want to go as far as the above. But very often your objects are both known and cheap to move. So write the really simple, rather correct, Foo(Bar) and be done with all of the tomfoolery.

There is a case where you have N variables that are not cheap to move and you want efficiency, and you don't want to put the implementation in the header file.

Then you just write a type-erasing Bar creator that takes anything that can be used to create a Bar either directly, or via std::make_from_tuple, and stores the creation for a later date. It then uses RVO to directly construct the Bar in-place within the target location.

template<class T>
struct make {
  using maker_t = T(*)(void*);
  template<class Tuple>
  static maker_t make_tuple_maker() {
    return [](void* vtup)->T{
      return make_from_tuple<T>( std::forward<Tuple>(*static_cast<std::remove_reference_t<Tuple>*>(vtup)) );
    };
  }
  template<class U>
  static maker_t make_element_maker() {
    return [](void* velem)->T{
      return T( std::forward<U>(*static_cast<std::remove_reference_t<U>*>(velem)) );
    };
  }
  void* ptr = nullptr;
  maker_t maker = nullptr;
  template<class U,
    std::enable_if_t< std::is_constructible<T, U>{}, int> =0,
    std::enable_if_t<!std::is_same<std::decay_t<U>, make>{}, int> =0
  >
  make( U&& u ):
    ptr( (void*)std::addressof(u) ),
    maker( make_element_maker<U>() )
  {}
  template<class Tuple,
    std::enable_if_t< !std::is_constructible<T, Tuple>{}, int> =0,
    std::enable_if_t< !std::is_same<std::decay_t<Tuple>, make>{}, int> =0,
    std::enable_if_t<(0<=std::tuple_size<std::remove_reference_t<Tuple>>{}), int> = 0 // SFINAE test that Tuple is a tuple-like
    // TODO: SFINAE test that using Tuple to construct T works
  >
  make( Tuple&& tup ):
    ptr( std::addressof(tup) ),
    maker( make_tuple_maker<Tuple>() )
  {}
  T operator()() const {
    return maker(ptr);
  }
};

Code uses a C++17 feature, std::make_from_tuple, which is relatively easy to write in C++11. In C++17 guaranteed elision means it even works with non-movable types, which is really cool.

Live example.

Now you can write:

Foo( make<Bar> bar_in ):bar_( bar_in() ) {}

and the body of Foo::Foo can be moved out of the header file.

But that is more insane than the above alternatives.

Again, have you considered just writing Foo(Bar)?

like image 170
Yakk - Adam Nevraumont Avatar answered Sep 28 '22 08:09

Yakk - Adam Nevraumont