完美转发

59 阅读2分钟

完美转发的意义是利用模板实现更大程度的代码复用,不用为了左值右值和const写很多重载。主要能力是原封不动的把函数参数类型(包括左右值和cv限定)转发给另一个函数。 主要依赖于c++11引入的转发引用forward函数。

#include <iostream>
#include <utility>

void print(int& x)       { std::cout << "Lvalue overload: " << x << "\n"; }
void print(int&& x)      { std::cout << "Rvalue overload: " << x << "\n"; }

template<typename T>
void wrapper(T&& arg) {
    // 完美转发:将 arg 以“原始身份”传入 print
    print(std::forward<T>(arg));
}

int main() {
    int a = 10;
    wrapper(a);          // Lvalue
    wrapper(20);         // Rvalue
}
  1. 右值引用:右值引用形式上表现为Type &&,基于这个语法,能实现移动语义和万能转发。从形式上讲,如果Type是一个确定的类型,那么语义上讲也是一个右值引用,而如果Tyep是自动推导的,那语义上则是一个万能引用,能同时容纳左右值,cv。
  2. 引用折叠规则能调整&,从而使得传入左右值没有区别
  3. forward能保持值原属性进行传输

完美转发和引用一起就会有很有趣的问题:

#include <iostream>
#include <bits/stdc++.h>
using namespace std;

void test(string&x){
    cout<<"test"<<x<<endl;
}

void test(const string& x){
    cout<<"test const"<<x<<endl;
}

void test(string&& x){
    cout<<"test rvalue"<<x<<endl;
}


template<typename T>
void test1(T x){
    test(x);
}


template<typename T>
void test2(T&& x){
    test(x);
}


template<typename T>
void test3(T &&x){
    test(static_cast<T &&>(x));
}

int main() {
    string a = " abc";  // 左值
    const string b = " abc"; // 左值 + const
    auto c = std::move(string(" abc")); // 左值(使用右值版本构造的左值)
    string && d = std::move(string(" abc")); // 类型为右值引用的左值,没有新对象,临时的生命周期被延长到 `d` 的作用域结束。
    auto && e = std::move(string(" abc"));  // 类似为右值引用的左值
    string _f = " abc"; // move(_f)为右值

    test1(a); // test
    test1(b); // test
    test1(c); // test
    test1(d); // test
    test1(e); // test
    test1(std::move(_f)); // test
    cout<<"--------------------------------"<<endl;

    _f = " abc";
    test2(a); // test
    test2(b); // test const
    test2(c); // test
    test2(d); // test
    test2(e); // test 
    test2(std::move(_f)); // test
    cout<<"--------------------------------"<<endl;

    _f = " abc";
    test3(a); // test
    test3(b); // test const
    test3(c); // test
    test3(d); // test
    test3(e); // test
    test3(std::move(_f)); // test rvalue
}

这里有很多值得讨论的点:

  1. auto 推导过程中的左右值,需要注意的是,auto本身不会显式的变为右值引用,只有auto&&才会
  2. obj && 是右值引用类型,但其本身是一个左值,所以对d,e的使用都需要当作左值来计算
  3. T a 本质上是值传递,因此对于左值和右值,都会构造(拷贝/移动)一个新的值,const 虽然可以被T推导出来,但是由于本身是按值传递,顶层const会失效(没有意义了,因为复制了新的值,怎么变更都不会影响原值)
  4. T&& x 是万能引用,但不配合forward/static_cast,并不能达到完美转发