要理解移动语义和完美转发,首先必须理解:

  1. 右值引用
  2. 万能引用和引用折叠

移动语义

移动语义拷贝语义相对的,移动语义可以将资源从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁。

完美转发的实现主要依赖于两个关键要素:右值引用移动构造函数/移动赋值操作符

在转移资源后,被移动的对象处于“有效但未定义的状态”(valid but unspecified state)

struct Foo {
   Foo() { std::cout << "Constructed" << std::endl; }
   Foo(const Foo &) { std::cout << "Copy-constructed" << std::endl; }
   Foo(Foo &&) { std::cout << "Move-constructed" << std::endl; }
   ~Foo() {}
};

int main()
{
    // 默认构造函数
    Foo f1;

    // 拷贝构造函数
    Foo f2 = f1;

    // 调用移动构造函数:将左值f1强制转换为将亡值,
    // 调用移动构造函数构造一个新对象。这里发生了资源所有权的转移。
    Foo f3 = std::move(f2);

    // 没有调用构造函数:声明并初始化一个引用,
    // 而不是创建一个新对象,没有发生任何资源转移,只是延长了这个将亡值的生命周期。
    Foo&& f4 = std::move(f3);
}

output:

Constructed
Copy-constructed
Move-constructed

std::move

std::move是一个标准库函数,它的作用就是将一个左值强制转换为右值引用,除此之外,没有其他处理。

// 这里的move(T&& t)代表万能引用
template<typename T>
constexpr typename std::remove_reference<T>::type&& move(T&& t) noexcept
{
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

std::move并不代表它真的移动了什么,它只是一个类型转换,告诉编译器这个对象可以被移动。

完美转发

完美转发(Perfect Forwarding)允许将函数模板中的参数完全转发给另一个函数,同时保持原始参数的值类别和常量性。

完美转发的实现主要依赖于两个关键要素:右值引用std::forward函数模板

std::forward

我们先来看std::forward的实现:

  • typename std::remove_reference::type&将T的引用属性剥除,使得t只能是一个左值引用或者右值引用,从而可以精确匹配std::forward的2个重载版本。
  • std::forward的返回类型是T&&万能引用,这个返回类型能根据引用折叠规则,使得:
    • && + & -> &
    • && + && -> &&
template<typename T>
constexpr T&& forward(typename std::remove_reference<T>::type& t) noexcept
{
    return static_cast<T&&>(t);  // 接收左值引用,返回万能引用
}

template<typename T>
constexpr T&& forward(typename std::remove_reference<T>::type&& t) noexcept
{
    return static_cast<T&&>(t);  // 接收右值引用,返回万能引用
}

举例说明:


void process(int& lval)  { /* 拷贝操作 */ }
void process(int&& rval) { /* 移动操作 */ }

template<typename T>
void wrapper(T&& arg) { // arg 是万能引用
    process(arg);       // 问题在这里
}

int main() {
    int x = 10;
    wrapper(x);         // 传入左值,arg 成为左值引用

    wrapper(20);        // 传入右值,arg 成为右值引用
}

上面例子中,当调用wrapper(20)

  1. 20是一个右值, T被推导为`int,T&&被实例化为int&&, 所以arg是一个右值引用。
  2. int&&右值引用类型是一个具名变量,所以arg被视为一个左值。这会导致process的拷贝版本被调用,而不是移动版本。

这里就导致了问题: 给wrapper传入了一个右值,但是在wrapper函数内部,却调用了process的左值版本。

std::forward为了解决这个问题,它可以让:左值进来,左值出去;右值进来,右值出去。

std::forward会根据模板参数T的类型信息,并结合引用折叠规则,进行有条件的类型转换。

注意:std::forward是利用模板参数T的类型,而不是arg的类型。

当在模板函数内部使用 std::forward<T>(arg) 时:

  • 如果原始参数是左值T 被推导为 int&std::forward<int&>(arg) 会返回 int& &&,根据引用折叠,最终类型是 int&,即左值引用
  • 如果原始参数是右值T 被推导为 intstd::forward<int>(arg) 会返回 int &&,即右值引用

std::forward 完美地保持了参数的原始属性。

void process(int& lval)  { /* 拷贝操作 */ }
void process(int&& rval) { /* 移动操作 */ }

template<typename T>
void wrapper(T&& arg) {
    process(std::forward<T>(arg)); // 使用 std::forward 完美转发
}

int main() {
    int x = 10;
    wrapper(x);         // 传入左值,std::forward转发为左值引用,调用process(int&)

    wrapper(20);        // 传入右值,std::forward转发为右值引用,调用process(int&&)
}

们再来做一个总结:

  • 第一个wrapper:
    • 当传入为x时,T被推导为int&,T&& 被推导为int&& &, 根据引用折叠,arg是一个左值引用,最终调用process的左值版本。
    • 当传入为10时,T被推导为int,T&& 被推导为int &&,arg是一个右值引用,也就是一个左值,最终还是调用process的左值版本。
  • 第二个wrapper:
    • 当传入为x时,T被推导为int&,编译器将std::forward实例化为std::forward<int&>(arg)std::forward返回值类型为T&&, 实例化后返回类型为int&& &, 根据引用折叠,最终返回类型为左值引用,调用process的左值版本。
    • 当传入为10时,T被推导为int,编译器将std::forward实例化为std::forward<int>(arg)std::forward返回值类型为T&&, 实例化后返回类型为int&&,最终返回类型为右值引用,调用process的右值版本。