移动语义和完美转发的区别

10

我已经从这个问题中了解了移动语义是什么: What are move semantics?

但是我还不知道与移动语义相关的完美转发是什么。

有人能用简单的英语和一个简单的例子解释一下什么是完美转发吗?


它们是正交概念。最好问“什么是完美转发”,而不是问它们之间的区别。 - juanchopanza
可能重复:https://dev59.com/m3A65IYBdhLWcg3w4C2j请仅返回翻译后的文本。 - sp2danny
@juanchopanza 的确是正交概念,不幸的是它们共享一个常见的语法 T&& :( - fredoverflow
2个回答

16

Plain English-only attempt

The problem is too complex to be accurately described by plain English sentences, but perfect forwarding can be thought of as a way to pass temporary values to another function without any unnecessary copies or assignments, as if the first function never existed. C++11 introduced conversion rules between r-value (&&) and l-value (&) references to allow for this.

C++11's r-value references address both move semantics and perfect forwarding issues.

This plain-English explanation only scratches the surface of the issue. For a more thorough understanding, it is suggested to read further:

The problem:

In passing temporary values to function F, there is a desire to avoid copying and assigning values in order to pass them to function E.

Attempts to solve this issue:

  • If you try to pass it by reference like

    template<typename T> void F(T& a) { E(a); }
    

    you will not be able to use temporaries (they're not l-values)

    F(1, 2, 3); // Won't work
    
  • Declaring a reference as const prolongs the lifetime of a temporary on the stack (this was historically done to avoid a common dangling reference error) so the following works

    template<typename T> void E(const T& a) {}
    
    template<typename T> void F(const T& a) {
        E(a);
    }
    

    but the downside is that you'll have to modify the signature of the function(s) to conform to this solution

  • If we're interested in the signature of E (it should conform to something) but not in F's one, we might get away with

    template<typename T> void E(T& a) {}
    
    template<typename T> void F(const T& a) {
        E(const_cast<T&>(a));
    }
    

    but in case this gets called with a real const and gets un-constant'ed, that would trigger undefined behavior

  • An unmaintainable solution could be to define all the variants you need

    template<typename T> void E(T& a) {}
    
    template<typename T> void F(T& a) { E(a); }
    template<typename T> void F(const T& a) { E(const_cast<T&>(a)); }
    

    but as the number of parameters grow, the number of combinations grows as well: this is likely to become unmaintainable

C++11的解决方案

C++11定义了一些规则,规定:

"[给定]一个类型TR是T类型的引用,试图创建类型“对cv TR的左值引用”会创建类型“对T的左值引用”,而试图创建类型“对cv TR的右值引用”会创建类型TR。"

以人类语言表述(TR = 类型T的引用,R = 引用):

TR      R
T& & -> T&    // an lvalue reference to cv TR (becomes)-> lvalue reference to T 
T& && -> T&   // an rvalue reference to cv TR (becomes)-> TR (lvalue reference to T) 
T&& & -> T&   // an lvalue reference to cv TR (becomes)-> lvalue reference to T 
T&& && -> T&& // an rvalue reference to cv TR (becomes)-> TR (rvalue reference to T)

重要的一点是,现在你可以跟踪函数接收的类型:你可以接收一个左值并将相同的左值传递给E,或者你可以接收一个右值并将相同的右值(在转换后,因为对于任何类型引用的左值引用变成了左值引用)传递给E。
template<typename T> void E(T&& a) {}

template<typename T> void F(T&& a) { E(static_cast<T&&>(a)); }

一种语法糖,用于
static_cast<T&&>(a)

std::forward<T>(a); // is the same as static_cast<T&&>(a);

所以最终解决问题且让您的生活更轻松的代码是:

template<typename T> void E(T&& a) {}

template<typename T> void F(T&& a) { E(std::forward<T>(a)); }

实时例子


参考资料:Herb Sutter的博客和其他一些来源,不幸的是我找不到它们了。如果有人知道,请在下面的评论中写下来,我会更新这篇文章。谢谢。


如果所有完美转发基本上只是一个强制转换,为什么我们还需要编写该强制转换呢?template<typename T> void F(T&& a) { E(a); }不也可以吗?通过说std::forward<T>(a);,我们向编译器提供了哪些额外信息? - j00hi

3
处理r-value引用和引用折叠可能比起初看起来更加复杂。 完美转发 完美转发是为了确保传递给函数的参数与最初提供的值类别相同(基本上是r-value vs l-value)而转发到另一个函数。
它通常与模板函数一起使用,其中可能已经发生了引用折叠。
它也可以在同一个函数中使用

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

例子

来自上述网站的一个例子,典型的例子是 make_unique

template<class T, class... U>
std::unique_ptr<T> make_unique(U&&... u)
{
    return std::unique_ptr<T>(new T(std::forward<U>(u)...));
}

在这个例子中,unique_ptr 的参数是通过 make_unique 直接提供给它的,就好像这些参数直接提供给了 unique_ptr 一样,即保持了参数的引用、左值和右值特性。
一个更具体的例子:
#include <iostream>
#include <utility>
#include <memory>

struct A {
  // implementation excluded
};

struct B {
  B(A &) // ctor 1
  {
    std::cout << "ctor 1" << std::endl;
  }
  B(A&&) // ctor 2
  {
    std::cout << "ctor 2" << std::endl;
  }
};

int main()
{
  A a;
  auto b1 = std::make_unique<B>(a); // ctor 1 is used
  auto b2 = std::make_unique<B>(A()); // ctor 2 is used
}

简述

完美转发依赖于一些 C++11 中新引入的基础语言构造,这些构造是我们现在看到的大多数泛型编程的基础:

  • 引用折叠
  • Rvalue 引用
  • 移动语义

目前使用 std::forward 的方式是公式化的 std::forward<T>,了解 std::forward 的工作原理有助于理解为什么这样做,并帮助识别 rvalue、引用折叠等非成语化或不正确的使用。

Thomas Becker 在完美转发问题和解决方案上提供了一个简洁但密集的 写作


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