【问题标题】:Why use a perfectly forwarded value (a functor)?为什么要使用完美转发的值(仿函数)?
【发布时间】:2014-09-06 22:19:09
【问题描述】:

C++11(和 C++14)引入了针对泛型编程的其他语言结构和改进。其中包括以下功能:

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

我正在浏览 C++14 specification 的早期 draft(现在带有更新的文本)和第 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

问题

  • 为什么apply_impl中的函数f会被转发,即为什么std::forward&lt;F&gt;(f)(std::get...
  • 为什么不直接将函数应用为f(std::get...

【问题讨论】:

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


    【解决方案1】:

    简介...

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

    当调用apply_implapply_impl_2 时,无论是使用右值Functor1() 还是使用左值func,输出都如预期的那样,调用了重载的调用运算符。 r 值和 l 值都调用它。在 C++03 下,这就是你所得到的,你不能基于对象的“r-value-ness”或“l-value-ness”重载成员方法。

    Functor1 ... 1
    Functor1 ... 2
    Functor1 ... 3
    函子1 ... 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_2id 5 和 6)的情况下,输出可能与最初预期的不同。在这两种情况下,都会调用符合条件的左值operator()(根本不调用右值)。可能已经预料到,由于使用右值Functor2() 来调用apply_impl_2,因此将调用限定为operator() 的右值。 func,作为apply_impl_2 的命名参数,是一个右值引用,但由于它被命名,它本身就是一个左值。因此,左值 operator()(int) const&amp; 被称为左值 func2 作为参数和右值 Functor2() 被用作参数的情况。

    apply_implid 7 和 8)的情况下,std::forward&lt;F&gt;(func)保持或保留func 提供的参数的右值/左值性质。因此,当右值Functor2() 用作参数时,左值限定operator()(int) const&amp; 被调用,左值func2 用作参数,右值限定operator()(int)&amp;&amp;。这种行为是意料之中的。

    结论

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

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

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

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

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

    推论和逆向

    • 这个问题的必然结果是;给定模板函数中的引用折叠,如何保持参数的右值/左值性质?答案 - 使用std::forward&lt;T&gt;(t)
    • 匡威; std::forward 能解决你所有的“通用参考”问题吗?不,它没有,在某些情况下should not be used,例如多次转发值。

    完美转发的简要背景

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

    简而言之,完美转发是为了确保提供给函数的参数被转发(传递)到与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 新的基本语言结构,它们构成了我们现在在泛型编程中看到的大部分内容的基础:

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

    std::forward 的使用目前是在公式化的std::forward&lt;T&gt; 中使用的,了解std::forward 的工作原理有助于理解为什么会这样,也有助于识别右值的非惯用或错误使用、引用折叠等.

    Thomas Becker 提供了一篇关于完美转发 problemsolution 的精彩但密集的文章。

    什么是引用限定符?

    引用限定符(左值引用限定符 &amp; 和右值引用限定符 &amp;&amp;)与 cv 限定符相似,因为它们(ref-qualified members)在 overload resolution 期间用于确定哪种方法打电话。他们的行为与您期望的一样; &amp; 适用于左值,&amp;&amp; 适用于右值。 注意:与 cv-qualification 不同,*this 仍然是一个左值表达式。

    【讨论】:

    • 今天我发现了 ref-qualified 方法。我的心被炸了。
    • SO 上Advantages of forward 的附加链接
    【解决方案2】:

    这是一个实际的例子。

    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

    如果在右值上下文中求值,则将 moves 赋值为临时变量,否则返回 const&amp; 到内部向量。

    现在我们打电话给apply

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

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

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

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

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-02-04
      • 2016-07-03
      • 1970-01-01
      • 2018-08-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多