第15课 完美转发(std::forward)

浪尽此生 提交于 2019-11-26 14:58:06

一. 理解引用折叠 

(一)引用折叠

  1. 在C++中,“引用的引用”是非法的。像auto& &rx = x;(注意两个&之间有空格)像这种直接定义引用的引用是不合法的,但是编译器在通过类型别名或模板参数推导等语境中,会间接定义出“引用的引用”,这时引用会形成“折叠”。

  2. 引用折叠会发生在模板实例化、auto类型推导、创建和运用typedef和别名声明、以及decltype语境中

(二)引用折叠规则

  1. 两条规则

    (1)所有右值引用折叠到右值引用上仍然是一个右值引用。如X&& &&折叠为X&&。

    (2)所有的其他引用类型之间的折叠都将变成左值引用。如X& &, X& &&, X&& &折叠为X&。可见左值引用会传染,沾上一个左值引用就变左值引用了根本原因:在一处声明为左值,就说明该对象为持久对象,编译器就必须保证此对象可靠(左值)

  2. 利用引用折叠进行万能引用初始化类型推导

    (1)当万能引用(T&& param)绑定到左值时,由于万能引用也是一个引用,而左值只能绑定到左值引用。因此,T会被推导为T&类型。从而param的类型为T& &&,引用折叠后的类型为T&。

    (2)当万能引用(T&& param)绑定到右值时,同理万能引用是一个引用,而右值只能绑定到右值引用上,故T会被推导为T类型。从而param的类型就是T&&(右值引用)。

【编程实验】引用折叠

#include <iostream>

using namespace std;

class Widget{};

template<typename T>
void func(T&& param){}

//Widget工厂函数
Widget widgetFactory() 
{
    return Widget();
}

//类型别名
template<typename T>
class Foo
{
public:
    typedef T&& RvalueRefToT;
};

int main()
{
    int x = 0;
    int& rx = x;
    //auto& & r = x; //error,声明“引用的引用”是非法的!

    //1. 引用折叠发生的语境1——模板实例化
    Widget w1;
    func(w1); //w1为左值,T被推导为Widget&。代入得void func(Widget& && param);
              //引用折叠后得void func(Widget& param)

    func(widgetFactory()); //传入右值,T被推导为Widget,代入得void func(Widget&& param)
                           //注意这里没有发生引用的折叠。

    //2. 引用折叠发生的语境2——auto类型推导
    auto&& w2 = w1; //w1为左值auto被推导为Widget&,代入得Widget& && w2,折叠后为Widget& w2
    auto&& w3 = widgetFactory(); //函数返回Widget,为右值,auto被推导为Widget,代入得Widget w3

    //3. 引用折叠发生的语境3——tyedef和using
    Foo<int&> f1;  //T被推导为int&,代入得typedef int& && RvalueRefToT;折叠后为typedef int& RvalueRefToT

    //4. 引用折叠发生的语境3——decltype
    decltype(x)&& var1 = 10;  //由于x为int类型,代入得int&& rx。
    decltype(rx) && var2 = x; //由于rx为int&类型,代入得int& && var2,折叠后得int& var2

    return 0;
}

二、完美转发

(一)std::forward原型

template<typename T>
T&& forward(typename remove_reference<T>::type& param)
{
    return static_cast<T&&>(param); //可能会发生引用折叠!
}

(二)分析std::forward<T>实现条件转发的原理(以转发Widget类对象为例

 

  1. 当传递给func函数的实参类型为左值Widget时,T被推导为Widget&类别。然后forward会实例化为std::forward<Widget&>,并返回Widget&(左值引用,根据定义是个左值!

  2. 而当传递给func函数的实参类型为右值Widget时,T被推导为Widget。然后forward被实例化为std::forward<Widget>,并返回Widget&&(注意,匿名的右值引用是个右值!)

  3. 可见,std::forward会根据传递给func函数实参(注意,不是形参)的左/右值类型进行转发当传给func函数左值实参时,forward返回左值引用,并将该左值转发给process。而当传入func的实参为右值时,forward返回右值引用,并将该右值转发给process函数。

【编程实验】不完美转发和完美转发

#include <iostream>
using namespace std;

void print(const int& t)  //左值版本
{
    cout <<"void print(const int& t)" << endl;
}

void print(int&& t)     //右值版本
{
    cout << "void print(int&& t)" << endl;
}

template<typename T>
void testForward(T&& param)
{
    //不完美转发
    print(param);            //param为形参,是左值。调用void print(const int& t)
    print(std::move(param)); //转为右值。调用void print(int&& t)

    //完美转发
    print(std::forward<T>(param)); //只有这里才会根据传入param的实参类型的左右值进转发
}

int main()
{
    cout <<"-------------testForward(1)-------------" <<endl;
    testForward(1);    //传入右值

    cout <<"-------------testForward(x)-------------" << endl;
    int x = 0;
    testForward(x);    //传入左值

    return 0;
}
/*输出结果
-------------testForward(1)-------------
void print(const int& t)
void print(int&& t)
void print(int&& t)       //完美转发,这里转入的1为右值,调用右值版本的print
-------------testForward(x)-------------
void print(const int& t)
void print(int&& t)
void print(const int& t) //完美转发,这里转入的x为左值,调用左值版本的print
*/

三、std::move和std::forward

(一)两者比较

  1. move和forward都是仅仅执行强制类型转换的函数。std::move无条件地将实参强制转换成右值。而std::forward则仅在某个特定条件满足时(传入func的实参是右值时)才执行强制转换

  2. std::move并不进行任何移动,std::forward也不进行任何转发。这两者在运行期都无所作为。它们不会生成任何可执行代码,连一个字节都不会生成。

(二)使用时机

  1. 针对右值引用的最后一次使用实施std::move,针对万能引用的最后一次使用实施std::forward

  2. 在按值返回的函数中,如果返回的是一个绑定到右值引用或万能引用的对象时,可以实施std::move或std::forward。因为如果原始对象是一个右值,它的值就应当被移动到返回值上,而如果是左值,就必须通过复制构造出副本作为返回值。

(三)返回值优化(RVO)

  1.两个前提条件

    (1)局部对象类型和函数返回值类型相同

    (2)返回的就是局部对象本身(含局部对象或作为return 语句中的临时对象等)

  2. 注意事项

    (1)在RVO的前提条件被满足时,要么避免复制,要么会自动地用std::move隐式实施于返回值

    (2)按值传递的函数形参,把它们作为函数返回值时,情况与返回值优化类似。编译器这里会选择第2种处理方案,即返回时将形参转为右值处理

    (3)如果局部变量有资格进行RVO优化,就不要把std::move或std::forward用在这些局部变量中。因为这可能会让返回值丧失优化的机会。

【编程实验】RVO优化和std::move、std::forward

#include <iostream>
#include <memory>
using namespace std;

//1. 针对右值引用实施std::move,针对万能引用实施std::forward
class Data{};

class Widget
{
    std::string name;
    std::shared_ptr<Data> ptr;
public:
    Widget() { cout <<"Widget()"<<endl; };

    //复制构造函数
    Widget(const Widget& w):name(w.name), ptr(w.ptr)
    {
        cout <<"Widget(const Widget& w)" << endl;
    }
    //针对右值引用使用std::move
    Widget(Widget&& rhs) noexcept: name(std::move(rhs.name)), ptr(std::move(rhs.ptr))
    {
        cout << "Widget(Widget&& rhs)" << endl;
    }

    //针对万能引用使用std::forward。
    //注意,这里使用万能引用来替代两个重载版本:void setName(const string&)和void setName(string&&)
    //好处就是当使用字符串字面量时,万能引用版本的效率更高。如w.setName("SantaClaus"),此时字符串会被
    //推导为const char(&)[11]类型,然后直接转给setName函数(可以避免先通过字量面构造临时string对象)。
    //并将该类型直接转给name的构造函数,节省了一个构造和释放临时对象的开销,效率更高。
    template<typename T>
    void setName(T&& newName)
    {
        if (newName != name) { //第1次使用newName
            name = std::forward<T>(newName); //针对万能引用的最后一次使用实施forward
        }
    }
};

//2. 按值返回函数
//2.1 按值返回的是一个绑定到右值引用的对象
class Complex 
{
    double x;
    double y;
public:
    Complex(double x =0, double y=0):x(x),y(y){}
    Complex& operator+=(const Complex& rhs) 
    {
        x += rhs.x;
        y += rhs.y;
        return *this;
    }
};

Complex operator+(Complex&& lhs, const Complex& rhs) //重载全局operator+
{
    lhs += rhs;
    return std::move(lhs); //由于lhs绑定到一个右值引用,这里可以移动到返回值上。
}

//2.2 按值返回一个绑定到万能引用的对象
template<typename T>
auto test(T&& t)
{
    return std::forward<T>(t); //由于t是一个万能引用对象。按值返回时实施std::forward
                               //如果原对象一是个右值,则被移动到返回值上。如果原对象
                               //是个左值,则会被拷贝到返回值上。
}

//3. RVO优化
//3.1 返回局部对象
Widget makeWidget()
{
    Widget w;

    return w;  //返回局部对象,满足RVO优化两个条件。为避免复制,会直接在返回值内存上创建w对象。
               //但如果改成return std::move(w)时,由于返回值类型不同(Widget右值引用,一个是Widget)
               //会剥夺RVO优化的机会,就会先创建w局部对象,再移动给返回值,无形中增加一个移动操作。
               //对于这种满足RVO条件的,当某些情况下无法避免复制的(如多路返回),编译器仍会默认地对
               //将w转为右值,即return std::move(w),而无须用户显式std::move!!!
}

//3.2 按值形参作为返回值
Widget makeWidget(Widget w) //注意,形参w是按值传参的。
{
    //...

    return w; //这里虽然不满足RVO条件(w是形参,不是函数内的局部对象),但仍然会被编译器优化。
              //这里会默认地转换为右值,即return std::move(w)
}

int main()
{
    cout <<"1. 针对右值引用实施std::move,针对万能引用实施std::forward" << endl;
    Widget w;
    w.setName("SantaClaus");

    cout << "2. 按值返回时" << endl;
    auto t1 = test(w); 
    auto t2 = test(std::move(w));

    cout << "3. RVO优化" << endl;
    Widget w1 = makeWidget();   //按值返回局部对象(RVO)
    Widget w2 = makeWidget(w1); //按值返回按值形参对象

    return 0;
}
/*输出结果
1. 针对右值引用实施std::move,针对万能引用实施std::forward
Widget()
2. 按值返回时
Widget(const Widget& w)
Widget(Widget&& rhs)
3. RVO优化
Widget()
Widget(Widget&& rhs)
Widget(const Widget& w)
Widget(Widget&& rhs)
*/
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!