C++11 右值引用、移动语义、完美转发

左值、右值

左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象。所有的具名变量或者对象都是左值,而右值不具名。
很难得到左值和右值的真正定义,但是有一个可以区分左值和右值的便捷方法:

看能不能对表达式取地址,如果能,则为左值,否则为右值。

int i = 0; // i是左值,0是右值
 
class Base 
{
public:
    int base;
};
Base getBase()
{
    return Base();
}
Base b = getBase(); // b是左值,getBase()的返回值是右值(临时变量)

左值引用、右值引用

C++98中的引用很常见了,就是给变量取了个别名,在C++11中,因为增加了右值引用(rvalue reference)的概念,所以C++98中的引用都称为了左值引用(lvalue reference)。C++11中的右值引用使用的符号是&&。
右值引用就是对一个右值进行引用的类型。由于右值通常不具有名字,所以我们一般只能通过右值表达式获得其引用。
比如:T && a=ReturnRvalue()。假设ReturnRvalue()函数返回一个右值,那么上述语句声明了一个名为a的右值引用,其值等于ReturnRvalue函数返回的临时变量的值。
基于右值引用可以实现移动语义和完美转发新特性。

int a = 1;
int& b = a;            //b是a的别名,a是左值。
int& c = 1;            //编译错误! 1是右值,不能够赋值给左值引用
 
int&& d = 1;           //实质上就是将不具名(匿名)变量取了个别名
int&& e = a;           //编译错误!a是左值,不能将一个左值赋值给一个右值引用
 
class Base 
{
public:
    int base;
};
Base getBase()
{
    return Base();
}
Base&& base = getBase(); // getBase()的返回值是右值(临时变量)

getBase返回的右值本来在表达式语句结束后,其生命也就该终结了(因为是临时变量),而通过右值引用,该右值又重获新生,其生命期将与右值引用类型变量base的生命期一样,只要base还活着,该右值临时变量将会一直存活下去。实际上就是给那个临时变量取了个名字。
注意:这里base的类型是右值引用类型(int &&),但是如果从左值和右值的角度区分它,它实际上是个左值。因为可以对它取地址,而且它还有名字,是一个已经命名的右值。

总结一下,其中T是一个具体类型:

  1. 左值引用,使用 T&,只能绑定左值
  2. 右值引用,使用 T&&,只能绑定右值
  3. 常量左值,使用 const T&,既可以绑定左值又可以绑定右值
  4. 已命名的右值引用,编译器会认为是个左值
  5. 编译器有返回值优化,但不要过于依赖

移动语义

class Base
{
public:
    Base(): data(new int(0)) { }
    // Base(const Base& base): data(base.data) { } // 如果不手写深拷贝构造函数,这是自动生成的默认的拷贝构造
    Base(const Base& base) {data = new int(*base.data);}
    ~Base() {std::cout << "~Base()" << std::endl;}
private:
    int* data;
};

对于一个包含指针成员变量的类,我们一般需要通过实现深拷贝的拷贝构造函数,为指针成员分配新的内存并进行内容拷贝,从而避免悬挂指针的问题。
但是如下列代码:

Base getBase()
{
    return Base(); // 无参构造一次,临时变量拷贝构造第一次(本次可能会被编译器优化)
}
Base base = getBase(); // 赋值给base拷贝构造第二次

这样拷贝构造函数一共被调用了两次,申请空间又释放内存,效率比较低。
C++11使用移动构造函数,从而保证使用临时对象构造a时不分配内存,从而提高性能。移动构造函数接收一个右值引用作为参数,使用右值引用的参数初始化其指针成员变量,将原来的指针成员指向nullptr,这样能够避免多次的申请释放。

class Base
{
public:
    Base(): data(new int(0)) { }
    // Base(const Base& base): data(base.data) { } // 如果不手写深拷贝构造函数,这是自动生成的默认的拷贝构造
    Base(const Base& base) {data = new int(*base.data);}
    Base(Base&& base): data(base.data) {base.data = nullptr;}
    ~Base() {std::cout << "~Base()" << std::endl;}
private:
    int* data;
};
Base getBase()
{
    return Base(); // 无参构造一次,临时变量拷贝构造(本次可能会被编译器优化)
}
Base base = getBase(); // 赋值给base移动构造

C++11提供了std::move()方法来将左值转换为右值。

class Base
{
public:
    Base(): data(new int(0))
    {
        std::cout << "无参构造" << std::endl;
    }
    Base(const Base& base)
    {
        data = new int(*base.data);
        std::cout << "拷贝构造" << std::endl;
    }
    Base(Base&& base): data(base.data)
    {
        base.data = nullptr;
        std::cout << "移动构造" << std::endl;
    }
     // 当存在移动构造函数、移动赋值函数时,默认的拷贝赋值会被标记成deleted,调用时编译报错,需要手写实现
    Base& operator=(const Base& base)
    {
        data = new int(*base.data);
        std::cout << "拷贝赋值" << std::endl;
        return *this;
    }
    Base& operator=(Base&& base)
    {
        data = base.data;
        base.data = nullptr;
        std::cout << "移动赋值" << std::endl;
        return *this;
    }
    ~Base()
    {
        std::cout << "~Base()" << std::endl;
    }
private:
    int* data;
};

Base base1 = Base();
Base base2 = Base();
Base base3(base1); // 调用拷贝构造函数
Base base4(std::move(base1)); // 调用移动构造函数,通过std::move()方法把左值转换成右值,再调用移动构造函数把右值绑定到右值引用上。
// 此时base1的内部指针已经失效了!不要使用

Base base5;
base5 = base2; // 调用拷贝赋值函数
Base base6;
base6 = std::move(base2); // 调用移动赋值函数,通过std::move()方法把左值转换成右值,再调用移动赋值函数把右值绑定到右值引用上。
// 此时base2的内部指针已经失效了!不要使用

如果我们没有提供移动构造函数,只提供了拷贝构造函数,std::move()会失效但是不会发生错误,因为编译器找不到移动构造函数就去寻找拷贝构造函数,也这是拷贝构造函数的参数是const T&常量左值引用的原因!(常量左值,使用 const T&,既可以绑定左值又可以绑定右值)

C++11中的所有容器都实现了移动构造函数。move只是转移了资源的控制权,本质上是将左值强制转化为右值使用,以用于移动拷贝或赋值,避免对含有资源的对象发生无谓的拷贝。
move对于拥有如内存、文件句柄等资源的成员的对象有效,但如果是一些基本类型,如int和char[10]数组等,如果使用move,仍会发生拷贝(因为没有对应的移动构造函数),所以说move对含有资源的对象说更有意义。

完美转发

完美转发指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。

template<typename T>
void function(T t) 
{
    otherdef(t);
}

如上所示,function() 函数模板中调用了 otherdef() 函数。在此基础上,完美转发指的是:

  • 如果 function() 函数接收到的参数 t 为左值,那么该函数传递给 otherdef() 的参数 t 也是左值;
  • 如果 function() 函数接收到的参数 t 为右值,那么传递给 otherdef() 函数的参数 t 也必须为右值。

显然,function() 函数模板并没有实现完美转发,有以下原因:

  • 参数 t 为非引用类型,这意味着在调用 function() 函数时,实参将值传递给形参的过程就需要额外进行一次拷贝操作;
  • 无论调用 function() 函数模板时传递给参数 t 的是左值还是右值,对于函数内部的参数 t 来说,它有自己的名称,也可以获取它的存储地址,因此它永远都是左值,也就是说,传递给 otherdef() 函数的参数 t 永远都是左值。

C++11 标准引入了右值引用和移动语义,因此很多场景中是否实现完美转发,直接决定了该参数的传递过程使用的是拷贝语义(调用拷贝构造函数)还是移动语义(调用移动构造函数)。

C++11 标准中规定,通常情况下右值引用形式的参数只能接收右值,不能接收左值。但对于函数模板中使用右值引用语法定义的参数来说,它不再遵守这一规定,既可以接收右值,也可以接收左值(此时的右值引用又被称为“万能引用”)。
仍以 function() 函数为例,在 C++11 标准中实现完美转发,只需要编写如下一个模板函数即可:

template <typename T>
void function(T&& t) 
{
    otherdef(t);
}

此模板函数的参数 t 既可以接收左值,也可以接收右值。但仅仅使用右值引用作为函数模板的参数是远远不够的,还有一个问题继续解决,即如果调用 function() 函数时为其传递一个左值引用或者右值引用的实参,如下所示:

int n = 10;
int & num = n;
function(num); // T 为 int&
 
int && num2 = 11;
function(num2); // T 为 int &&

其中,由 function(num) 实例化的函数底层就变成了 function(int & & t),同样由 function(num2) 实例化的函数底层则变成了 function(int && && t)。要知道,C++98/03 标准是不支持这种用法的,而 C++ 11标准为了更好地实现完美转发,特意为其指定了新的类型匹配规则,又称为引用折叠规则(假设用 A 表示实际传递参数的类型):

  • 当实参为左值或者左值引用(A&)时,函数模板中 T&& 将转变为 A&(A& && = A&);
  • 当实参为右值或者右值引用(A&&)时,函数模板中 T&& 将转变为 A&&(A&& && = A&&)。

在实现完美转发时,只要函数模板的参数类型为 T&&,则 C++ 可以自行准确地判定出实际传入的实参是左值还是右值。

通过将函数模板的形参类型设置为 T&&,我们可以很好地解决接收左、右值的问题。但除此之外,还需要解决一个问题,即无论传入的形参是左值还是右值,对于函数模板内部来说,形参既有名称又能寻址,因此它都是左值。那么如何才能将函数模板接收到的形参连同其左、右值属性,一起传递给被调用的函数呢?
C++11 标准的开发者已经帮我们想好的解决方案,该新标准还引入了一个模板函数 forword<T>(),我们只需要调用该函数,就可以很方便地解决此问题。仍以 function 模板函数为例,如下演示了该函数模板的用法:

#include <iostream>
using namespace std;
 
//重载被调用函数,查看完美转发的效果
void otherdef(int & t) 
{
    cout << "lvalue\n";
}
void otherdef(const int & t) 
{
    cout << "rvalue\n";
}
 
//实现完美转发的函数模板
template <typename T>
void function(T&& t) 
{
    otherdef(forward<T>(t));
}
 
int main()
{
    function(5);
    int x = 1;
    function(x);
    return 0;
}
 
//程序执行结果为:
 
//rvalue
//lvalue

此 function() 模板函数才是实现完美转发的最终版本。可以看到,forword() 函数模板用于修饰被调用函数中需要维持参数左、右值属性的参数。

总的来说,在定义模板函数时,我们采用右值引用的语法格式定义参数类型,由此该函数既可以接收外界传入的左值,也可以接收右值;其次,还需要使用 C++11 标准库提供的 forword() 模板函数修饰被调用函数中需要维持左、右值属性的参数。由此即可轻松实现函数模板中参数的完美转发。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容