使用std :: forward的构造函数

gri*_*dil 16 c++ move-semantics perfect-forwarding c++11

据我所知,在C++ 11中有效实现构造函数的两种常用方法是使用其中两种

Foo(const Bar& bar) : bar_{bar} {};
Foo(Bar&& bar)      : bar_{std::move(bar)} {};
Run Code Online (Sandbox Code Playgroud)

或者只是一个时尚的

Foo(Bar bar) : bar_{std::move(bar)} {};
Run Code Online (Sandbox Code Playgroud)

使用第一个选项产生最佳性能(例如,在左值的情况下希望是单个副本,在rvalue的情况下希望单个移动),但是对于N个变量需要2个N重载,而第二个选项只需要一个函数通过左值时的额外动作.

在大多数情况下,这不应该产生太大的影响,但肯定两种选择都不是最佳选择.但是,也可以执行以下操作:

template<typename T>
Foo(T&& bar) : bar_{std::forward<T>(bar)} {};
Run Code Online (Sandbox Code Playgroud)

这样做的缺点是允许可能不需要的类型的变量作为bar参数(这是我确信使用模板专门化很容易解决的问题),但无论如何,性能是最佳的,代码随着变量的数量线性增长.

为什么没有人为此目的使用像forward这样的东西?这不是最优化的方式吗?

Yak*_*ont 21

人们做了完美的前锋建设者.

有成本.

首先,成本是它们必须在头文件中.其次,每次使用都会导致创建不同的构造函数.第三,您不能{}对正在构造的对象使用类似的初始化语法.

第四,它Foo(Foo const&)Foo(Foo&&)构造者的交互很差.它不会取代它们(由于语言规则),但它将被选中Foo(Foo&).这可以通过一些样板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)} {};
Run Code Online (Sandbox Code Playgroud)

现在不再Foo(Foo const&)是类型参数的首选Foo&.我们在这里,我们可以做到:

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)}
{};
Run Code Online (Sandbox Code Playgroud)

现在这个构造函数只有在参数可用于构造时才有效bar.

接下来你要做的就是支持{}风格构造bar,或者分段构造,或者向前进入bar的varargs构造.

这是一个varargs变种:

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;
Run Code Online (Sandbox Code Playgroud)

另一方面,如果我们添加:

Foo(Bar&& bin):bar_(std::move(bin));
Run Code Online (Sandbox Code Playgroud)

我们现在支持Foo( {construct_bar_here} )语法,这很好.但是,如果我们已经拥有上述varardic(或类似的分段结构),则不需要这样做.尽管如此,有时候初始化列表很容易转发,特别是如果我们不知道bar_编写代码的时候(泛型,比方说):

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)...}
{};
Run Code Online (Sandbox Code Playgroud)

所以,如果Barstd::vector<int>我们可以做到Foo( {1,2,3} )并最终{1,2,3}在内bar_.

在这一点上,你必须想知道"我为什么不写Foo(Bar)".搬家真的那么贵Bar吗?

在通用的库式代码中,您将希望达到上述目的.但是,你的物品往往都是已知且移动便宜的.所以写出非常简单,相当正确,Foo(Bar)并完成所有的tomfoolery.

有一种情况,你有N个变量,移动不便宜,你想要效率,你不希望将实现放在头文件中.

然后,您只需编写一个类型擦除Bar创建器,它可以使用任何可用于Bar直接或通过创建的内容std::make_from_tuple,并将创建存储在以后的日期.然后,它使用RVO直接Bar在目标位置内构建就地.

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);
  }
};
Run Code Online (Sandbox Code Playgroud)

Code使用C++ 17特性,std::make_from_tuple在C++ 11中编写相对容易.在C++ 17中,保证elision意味着它甚至适用于不可移动的类型,这真的很酷.

实例.

现在你可以写:

Foo( make<Bar> bar_in ):bar_( bar_in() ) {}
Run Code Online (Sandbox Code Playgroud)

并且Foo::Foo可以将主体移出头文件.

但这比上述替代方案更疯狂.

你再考虑过写作Foo(Bar)吗?

  • 您也可以使用通用引用进行`{}`式初始化:`template <typename T = Bar> Foo(T && bar):...`.当参数有类型时,它会被使用.否则编译器会选择默认值并为您构造一个"Bar"rvalue. (3认同)