32

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());
}

在线[intseq.general]/2

问题

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

2 回答 2

49

简单来说...

TL;DR,您希望保留函子的值类别(r-value/l-value 性质),因为这会影响重载决议,特别是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)用 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)...);
}

样品评估

评估其行为方式的一些经验证据(使用符合标准的编译器)是评估代码示例为何如此编写的一个很好的起点。因此,另外我们将定义一个通用函子;

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);
}

并且输出与预期的一样,与调用和调用重载调用运算符时是使用 r 值还是使用Functor1()l 值无关。r 值和 l 值都调用它。在 C++03 下,这就是你所得到的,你不能重载基于对象的“r-value-ness”或“l-value-ness”的成员方法。funcapply_implapply_impl_2

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

Ref-qualified 样品

我们现在需要重载该调用运算符以进一步扩展它......

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 &... 5
Functor2 &... 6
Functor2 &... 7
Functor2 &&... 8

讨论

apply_impl_2( id5 和 6) 的情况下,输出可能不像最初预期的那样。在这两种情况下,都会调用限定operator()的左值(根本不调用右值)。可能已经预料到,因为Functor2(),一个 r 值,用于调用apply_impl_2合格的 r 值operator()将被调用。,func作为 的命名参数apply_impl_2,是一个右值引用,但由于它被命名,它本身就是一个左值。因此,在左值作为参数和右值用作参数operator()(int) const&的情况下,都会调用限定的左值。func2Functor2()

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

结论

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

它不仅需要、std::forward可以而且应该用于将参数转发给函数,而且还需要在必须保留 r-value/l-value 性质的情况下使用参数。笔记; 在某些情况下,不能或不应该保留 r 值/l 值,在这些情况下std::forward不应使用(参见下面的反面)。

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

编写定义良好且合理的通用代码一直很困难。随着 r 值引用的引入,尤其是引用折叠,可以更简洁地编写更好的通用代码,但我们需要更加了解所提供参数的原始性质是什么,并确保当我们在我们编写的通用代码中使用它们时,它们会被维护。

完整的示例代码可以在这里找到

推论和逆向

  • 这个问题的推论是;给定模板函数中的引用折叠,如何保持参数的右值/左值性质?答案 - 使用std::forward<T>(t)
  • 交谈; std::forward能解决你所有的“通用参考”问题吗?不,它没有,在某些情况下不应该使用它,例如多次转发值。

完美转发的简要背景

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

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

Scott Meyers 在他的Going Native 2013 演示文稿中给出了以下伪代码来解释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 新的基本语言结构,它们构成了我们现在在泛型编程中看到的大部分内容的基础:

  • 参考折叠
  • 右值引用
  • 移动语义

目前的使用std::forward是在公式化的std::forward<T>,理解如何std::forward工作有助于理解为什么会这样,并且还有助于识别右值的非惯用或不正确的使用、引用折叠等。

Thomas Becker 提供了一篇关于完美转发问题解决方案的精彩但密集的文章。

什么是 ref 限定符?

ref-qualifiers(lvalue ref-qualifier&和 rvalue ref-qualifier &&)与 cv-qualifiers 类似,因为它们(ref-qualified members)在重载决议期间用于确定调用哪个方法。他们的行为与您期望的一样;&适用于左值和右&&值。注意:与 cv-qualification 不同,*this它仍然是一个左值表达式。

于 2014-07-16T11:41:24.230 回答
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, 并将它连接到一个 internal std::vector。然后它返回那个std::vector.

如果在右值上下文中评估它move是一个临时的,否则它返回一个const&到内部向量。

现在我们调用apply

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

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

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

因为operator()&&知道函数对象应该被视为即将被销毁的右值,所以它可以在执行操作时从函数对象中取出内脏。operator()&不能这样做。

仔细使用函数对象的完美转发可以实现这种优化。

但是请注意,此时“在野外”很少使用这种技术。右值限定的重载是晦涩难懂的,这样做更是如此operator()

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

于 2014-07-16T15:10:55.933 回答