Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
109 views
in Technique[技术] by (71.8m points)

c++ - 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?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...