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

Nia*_*all 32 c++ perfect-forwarding c++11 c++14

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

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

我在浏览较早草案的的C++ 14规范(现在更新文本)和码中的示例中§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());
}
Run Code Online (Sandbox Code Playgroud)

在线这里[intseq.general]/2.

  • 为什么功能fapply_impl被转发,即为什么std::forward<F>(f)(std::get...
  • 为什么不直接应用该功能f(std::get...

Nia*_*all 48

简单来说...

TL; DR,您希望保留仿函数的值类别(r值/ l值性质),因为这会影响重载决策,特别是ref-qualified成员.

功能定义减少

为了关注正在转发的函数的问题,我减少了样本(并使其使用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)...);
}
Run Code Online (Sandbox Code Playgroud)

我们创建了第二种形式,我们将其替换为std::forward(func)just 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)...);
}
Run Code Online (Sandbox Code Playgroud)

样品评估

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

struct Functor1 {
  int operator()(int id) const
  {
    std::cout << "Functor1 ... " << id << std::endl;
    return id;
  }
};
Run Code Online (Sandbox Code Playgroud)

初始样本

运行一些示例代码;

int main()
{
  Functor1 func1;
  apply_impl_2(func1, 1);
  apply_impl_2(Functor1(), 2);
  apply_impl(func1, 3);
  apply_impl(Functor1(), 4);
}
Run Code Online (Sandbox Code Playgroud)

并且如预期的输出,独立的r值是否被用于Functor1()或1-值func进行呼叫时apply_implapply_impl_2重载呼叫操作被调用.它被称为r值和l值.在C++ 03下,这就是你所得到的,你不能基于对象的"r-value-ness"或"l-value-ness"来重载成员方法.

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

我们运行另一个样本集;

int main()
{
  Functor2 func2;
  apply_impl_2(func2, 5);
  apply_impl_2(Functor2(), 6);
  apply_impl(func2, 7);
  apply_impl(Functor2(), 8);
}
Run Code Online (Sandbox Code Playgroud)

而输出是;

Functor2&... 5
Functor2&... 6
Functor2&... 7
Functor2 && ... 8

讨论

apply_impl_2(id5和6)的情况下,输出不是最初预期的.在这两种情况下,operator()都会调用l值限定(根本不调用r值).可能已经预料到,因为Functor2()使用r值来调用已经调用apply_impl_2的r值operator().的func,作为命名参数apply_impl_2,是r值参考,但由于它被命名,它是本身的左值.因此operator()(int) const&,在l值为func2参数且r值Functor2()用作参数的情况下,都会调用l值限定.

apply_impl(id7和8)的情况下,std::forward<F>(func) 维持或保留所提供的参数的r值/ l值性质func.因此,operator()(int) const&使用l值func2作为参数调用l 值限定,并且operator()(int)&&当r值Functor2()用作参数时,r 值合格.这种行为是预期的.

结论

std::forward通过完美转发的使用确保我们保留原始参数的r值/ l值性质func.它保留了它们的价值范畴.

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

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

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

可以在此处找到完整的示例代码

推论和相反

  • 这个问题的必然结果是; 给定参考在模板化函数中折叠,参数的r值/ l值性质如何保持?答案 - 使用std::forward<T>(t).
  • 交谈; 确实std::forward解决了所有"普遍参考"问题?不,它没有,有些情况下不应该使用它,例如转发值不止一次.

完善转发的简要背景

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

简而言之,完美转发是为了确保提供给函数的参数被转发(传递)到具有与最初提供的相同值类别(基本上是r值与l值)的另一个函数.它通常与模板功能一起使用,其中可能发生了参考折叠.

Scott Meyers在2013年的Going Native演示文稿中给出了以下伪代码,以解释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);
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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

目前使用的std::forward是公式化std::forward<T>,了解std::forward作品如何帮助理解为什么会这样,并且还有助于识别rvalues的非惯用或不正确使用,参考崩溃和类似.

Thomas Becker为完美的转发问题解决方案提供了一个很好但密集的文章.

什么是参赛资格赛?

ref-qualifiers(左值ref-qualifier &和rvalue ref-qualifier &&)类似于cv-qualifiers,因为它们(ref-qualified成员)在重载决策期间用于确定调用哪个方法.他们表现得像你期望的那样; 在&适用于左值和&&到右值.注意:与cv-qualification不同,*this仍然是l值表达式.

  • 今天我发现了ref-qualified方法.我的思绪已被吹嘘. (14认同)

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

此函数对象接受x并将其连接到内部std::vector.然后它返回std::vector.

如果在rvalue上下文中计算它move是临时的,否则它返回一个const&内部向量.

现在我们打电话apply:

auto result = apply( concat{}, std::make_tuple(2) );
Run Code Online (Sandbox Code Playgroud)

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

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

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

仔细使用功能对象的完美转发可实现此优化.

但请注意,此时"在野外"几乎没有使用这种技术.Rvalue限定的重载是不明确的,并且这样做更多operator().

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