使用std::forward的构造函数

25
据我所知,在C++11中有效实现构造函数的两种常见方法是使用它们中的两个。
Foo(const Bar& bar) : bar_{bar} {};
Foo(Bar&& bar)      : bar_{std::move(bar)} {};

或者只是一种时尚风格

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

第一种选择可以实现最佳性能(例如,对于lvalue的情况希望只有一个副本,对于rvalue的情况希望只有一次移动),但需要2N个N变量的重载,而第二种选择只需要一个函数,但需要在传入lvalue时进行额外的移动。

在大多数情况下,这不会产生太大影响,但显然两种选择都不是最优的。然而,也可以采取以下措施:

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

这样做的缺点是允许可能不需要的类型作为bar参数(但我确信可以使用模板特化轻松解决此问题),但无论如何,性能是最优的,并且代码随变量数量呈线性增长。

为什么没有人像对于这个目的使用forward一样?这难道不是最优的方法吗?


3
该模板不允许隐式转换,例如从花括号中。有时将其提前转换为所需类型很方便。 - Kerrek SB
@KerrekSB 请回答,我迫不及待想知道。 - πάντα ῥεῖ
你也可以使用 typename = std::enable_if_t<std::is_same_v<std::remove_reference_t<T>, Bar>> 来限制类型。 - Tavian Barnes
2
你可以并且应该将 SFINAE 应用到模板构造函数中。 - NathanOliver
限制重载集,使得 is_constructible<Foo,X> 不对任何类型 X 都成立,实际上并不容易编写,特别是对于叶子代码和非模板熟练的用户。 - Kerrek SB
需要注意的一点是,这不能用于实现特殊成员函数 - 拷贝构造函数或移动构造函数 - 因为成员模板的特化不被视为拷贝构造函数或移动构造函数的实现。它们不会抑制上述特殊成员函数的隐式声明。 - AnT stands with Russia
1个回答

35

人们会使用完美转发构造函数。

这样做是有代价的。

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

第四,它与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)} {};

现在不再推荐使用 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)}
{};

现在这个构造函数只有在参数可用于构建bar时才起作用。

接下来你需要做的是支持{}样式的bar构建,或者分步构建,或者可变参数构建,在后者中你将转发到bar

这里是一个可变参数的变体:

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;

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

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

我们现在支持Foo( {construct_bar_here} )的语法,这很好。然而,如果我们已经有了上述可变参数(或类似的分段构造),则不需要此语法。尽管如此,有时转发初始化列表仍然很有用,特别是当我们编写代码时不知道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)...}
{};

所以如果Bar是一个std::vector<int>,我们可以使用Foo( {1,2,3} ),最终在bar_中得到{1,2,3}

此时,你可能会想“为什么我不直接写Foo(Bar)呢?”移动一个Bar真的那么昂贵吗?

在通用库式的代码中,你需要做到上述程度。但很多时候,你的对象既已知又便宜移动。因此,编写非常简单而正确的Foo(Bar)并完成所有愚蠢的事情。

有一种情况,你有N个不便宜移动的变量,你需要效率,并且你不想将实现放在头文件中。

然后,你只需编写一个类型擦除的Bar创建器,它接受任何可用于直接或通过std::make_from_tuple创建Bar的内容,并将创建存储到稍后的日期。然后,它使用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);
  }
};

代码使用了C++17的一个特性,std::make_from_tuple,在C++11中编写相对容易。 在C++17中,保证省略意味着它甚至可以与非可移动类型一起使用,这真是太棒了。

实时示例

现在你可以这样编写:

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

Foo::Foo的函数体可以移出头文件。

但是这比上面的方法更疯狂。

再次,你有没有考虑过只写Foo(Bar)


4
你也可以使用通用引用来进行类似 {} 的初始化:template<typename T = Bar> Foo(T &&bar):...。当实参具有类型时,它会被使用。否则,编译器会选择默认值并为你构造一个 Bar 右值。 - Constantin Baranov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接