为什么要使用完美转发的值(仿函数)?

时间:2014-07-16 11:41:24

标签: c++ c++11 perfect-forwarding c++14

C ++ 11(和C ++ 14)引入了针对通用编程的其他语言结构和改进。这些功能包括:

  • R值参考
  • 参考折叠
  • 完美转发
  • 移动语义,可变参数模板等

我正在浏览draft的早期C++14 specification(现在更新了文本)和§20.5.1中的示例中的代码,编译时整数序列 ,我发现有趣和奇特。

template<class F, class Tuple, std::size_t... I>
decltype(auto) apply_impl(F&& f, Tuple&& t, index_sequence<I...>) {
  return std::forward<F>(f)(std::get<I>(std::forward<Tuple>(t))...);
}

template<class F, class Tuple>
decltype(auto) apply(F&& f, Tuple&& t) {
  using Indices = make_index_sequence<std::tuple_size<Tuple>::value>;
  return apply_impl(std::forward<F>(f), std::forward<Tuple>(t), Indices());
}

在线[intseq.general]/2

问题

  • 为什么f中的apply_impl函数被转发,即为什么std::forward<F>(f)(std::get...
  • 为什么不将该功能应用为f(std::get...

2 个答案:

答案 0 :(得分:47)

简介......

TL; DR,您希望保留仿函数的value category(r值/ l值性质),因为这会影响overload resolution,尤其是ref-qualified members

功能定义减少

为了关注正在转发的函数的问题,我已经减少了样本(并使其使用C ++ 11编译器进行编译);

template<class F, class... Args>
auto apply_impl(F&& func, Args&&... args) -> decltype(std::forward<F>(func)(std::forward<Args>(args)...)) {
  return std::forward<F>(func)(std::forward<Args>(args)...);
}

我们创建了第二个表单,我们用std::forward(func)替换func;

template<class F, class... Args>
auto apply_impl_2(F&& func, Args&&... args) -> decltype(func(std::forward<Args>(args)...)) {
  return func(std::forward<Args>(args)...);
}

样本评估

评估一些关于这种行为(使用符合编译器)的经验证据是评估代码示例为何如此编写的一个简洁的起点。因此,我们将另外定义一个通用函子;

struct Functor1 {
  int operator()(int id) const
  {
    std::cout << "Functor1 ... " << id << std::endl;
    return id;
  }
};

初始样本

运行一些示例代码;

int main()
{
  Functor1 func1;
  apply_impl_2(func1, 1);
  apply_impl_2(Functor1(), 2);
  apply_impl(func1, 3);
  apply_impl(Functor1(), 4);
}

输出与预期一致,与调用Functor1()func时是否使用r值apply_impl或l值apply_impl_2无关调用重载的调用操作符。它被称为r值和l值。在C ++ 03下,这就是你所拥有的,你不能基于&#34; r-value-ness&#34;重载成员方法。或者&#34; l-value-ness&#34;对象。

  

Functor1 ... 1
  Functor1 ... 2
  Functor1 ... 3
  Functor1 ... 4

参考合格样品

我们现在需要重载该调用运算符以进一步延伸...

struct Functor2 {
  int operator()(int id) const &
  {
    std::cout << "Functor2 &... " << id << std::endl;
    return id;
  }
  int operator()(int id) &&
  {
    std::cout << "Functor2 &&... " << id << std::endl;
    return id;
  }
};

我们运行另一个样本集;

int main()
{
  Functor2 func2;
  apply_impl_2(func2, 5);
  apply_impl_2(Functor2(), 6);
  apply_impl(func2, 7);
  apply_impl(Functor2(), 8);
}

输出是;

  

Functor2&amp; ... 5
  Functor2&amp; ... 6
  Functor2&amp; ... 7
  Functor2&amp;&amp; ... 8

<强>讨论

apply_impl_2id 5和6)的情况下,输出不是最初可能的预期。在这两种情况下,都会调用限定为operator()的l值(根本不调用r值)。可能已经预期,由于Functor2()(一个r值)用于调用apply_impl_2,因此将调用符合条件的operator() r值。 func作为apply_impl_2的命名参数,是一个r值引用,但由于它是命名的,因此它本身就是一个l值。因此,在l值operator()(int) const&是参数和r值func2被用作参数的情况下,调用l值限定Functor2()

apply_implid 7和8)的情况下,std::forward<F>(func) 维护或保留参数的r值/ l值性质提供给func。因此,当使用r值operator()(int) const&时,使用l值func2作为参数调用l值限定operator()(int)&&,并使用r值限定Functor2()作为论点。这种行为是人们所期望的。

结论

通过完美转发使用std::forward可确保我们保留func原始参数的r值/ l值性质。它保留了value category

这是必需的,std::forward可以而且应该不仅仅用于转发函数的参数,而且当 r-需要使用参数时必须保留值/ l值性质。注意;在某些情况下,不能或不应保留r值/ l值,在这些情况下不应使用std::forward(参见下面的反向)。

有许多例子突然出现,通过看似无辜地使用r值引用而无意中失去了参数的r值/ l值性质。

编写定义明确且通用的通用代码一直很难。随着r值引用的引入,特别是引用折叠,已经有可能更简洁地编写更好的通用代码,但我们需要更加了解所提供的参数的原始性质,并确保当我们在我们编写的通用代码中使用它们时,它们会被维护。

可以找到完整的示例代码here

推论和反向

  • 问题的必然结果是;给定参考在模板化函数中折叠,参数的r值/ l值性质如何保持?答案 - 使用std::forward<T>(t)
  • 逆;是std::forward解决所有&#34;通用参考&#34;问题?不,它没有,有些情况下should not be used,例如转发价值不止一次。

完善转发的简要背景

有些人可能不熟悉完美转发,那么什么是完美转发

简而言之,完美转发是为了确保提供给函数的参数被转发(传递)到具有相同值类别(基本上是r值与l值)的另一个函数originally provided。它通常用于可能发生reference collapsing的模板函数。

Scott Meyers在Going Native 2013 presentation中提供了以下伪代码来解释std::forward的工作原理(大约在20分钟左右);

template <typename T>
T&& forward(T&& param) { // T&& here is formulated to disallow type deduction
  if (is_lvalue_reference<T>::value) {
    return param; // return type T&& collapses to T& in this case
  }
  else {
    return move(param);
  }
}

完美的转发取决于C ++ 11新增的一些基本语言结构,这些结构构成了我们现在在泛型编程中看到的大部分基础:

  • 参考折叠
  • Rvalue references
  • 移动语义

std::forward目前用于公式化std::forward<T>,了解std::forward的工作方式有助于理解为何如此,并有助于识别非惯用或不正确使用价值,参考崩溃和同类。

托马斯·贝克尔(Thomas Becker)在完美的转发problemsolution上提供了一篇不错但密集的文章。

什么是ref-qualifiers?

ref-qualifiers(左值ref-qualifier &和rvalue ref-qualifier &&)类似于cv-qualifiers,因为它们(ref-qualified members)在{期间}使用{3}}确定要调用的方法。他们表现得像你期望的那样; &适用于左值,&&适用于右值。 注意:与cv-qualification不同,*this仍为l值表达式。

答案 1 :(得分:13)

这是一个实际的例子。

struct concat {
  std::vector<int> state;
  std::vector<int> const& operator()(int x)&{
    state.push_back(x);
    return state;
  }
  std::vector<int> operator()(int x)&&{
    state.push_back(x);
    return std::move(state);
  }
  std::vector<int> const& operator()()&{ return state; }
  std::vector<int> operator()()&&{ return std::move(state); }
};

此函数对象采用x,并将其连接到内部std::vector。然后它返回std::vector

如果在右值上下文中计算它move为临时值,否则它会向内部向量返回const&

现在我们致电apply

auto result = apply( concat{}, std::make_tuple(2) );

因为我们小心地转发了我们的函数对象,所以只分配了1 std::vector个缓冲区。它只是移到result

如果不仔细转发,我们最终会创建一个内部std::vector,然后将其复制到result,然后放弃内部std::vector

因为operator()&&知道函数对象应该被视为要销毁的右值,所以它可以在执行操作时从函数对象中删除内容。 operator()&无法执行此操作。

小心使用功能对象的完美转发可实现此优化。

但请注意,这种技术很少使用#34;在野外#34;在此刻。 Rvalue限定的重载是模糊的,并且这样做operator()更多。

然而,我可以很容易地看到未来版本的C ++使用lambda的rvalue状态隐式地move在某些上下文中的值捕获数据。