右值引用:移动语义和完美转发

右值引用:移动语义和完美转发

指针成员与拷贝构造

#include <iostream>
using namespace std;

class HasPtrMem {
public:
    HasPtrMem(): d(new int(0)) {}
    ~HasPtrMem() { delete d; }
    int * d;
};

int main() {
    HasPtrMem a;
    HasPtrMem b(a);
    cout << *a.d << endl;   // 0
    cout << *b.d << endl;   // 0
}   // 析构:运行时错误,多次在同一位置调用delete
  • 浅拷贝(shollow copy)

在未声明拷贝构造函数时,编译器也会为类生成一个浅拷贝构造函数。

解决浅拷贝问题的方法是用户自定义拷贝函数,进行“深拷贝”(deep copy)。

#include <iostream>
using namespace std;

class HasPtrMem {
public:
    HasPtrMem(): d(new int(0)) {}
    HasPtrMem(const HasPtrMem & h):
        d(new int(*h.d)) {} // 拷贝构造函数,从堆中分配内存,并用*h.d初始化
    ~HasPtrMem() { delete d; }
    int * d;
};

int main() {
    HasPtrMem a;
    HasPtrMem b(a);
    cout << *a.d << endl;   // 0
    cout << *b.d << endl;   // 0
}   // 正常析构析构

左值,右值和右值引用

在C++11中所有的值必属于左值(lvalue)、右值(rvalue)两者之一,右值又可以细分为纯右值(prvalue, Pure RValue)、将亡值(xvalue, eXpiring Value)。

在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。

a = b+c;

// a 左值, &a 合法
// b+c 是右值, &(b+c) 非法

纯右值

  • 非引用返回的函数返回的临时变量值
  • 1+3 产生的临时变量
  • 2, 'c', true
  • 类型转换函数的返回值
  • lambda 表达式

将亡值

将亡值则是 C++11 新增的跟右值引用相关的表达式,这样表达式通常是将要被移动对象.

  • 返回右值引用 T&& 的函数返回值
  • std::move 的返回值
  • 转换为 T&& 的类型转换函数的返回值

右值引用就是对一个右值进行引用的类型.

通常情况下,右值不具有名字,我们只能通过引用的方式找到它.

T&& a = RetureRvalue();
/*
 * a 是右值引用
 * RetureRvalue 返回一个临时变量
 * a 这个右值引用 引用 RetureRvalue 返回的临时变量
*/

右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。

通常情况下,右值引用不能绑定到任何左值上.

int c;
int &&d = c; // error, 左值引用不能绑定到左值上

C++98 左值引用是否可以绑定到右值上?

T& e = RetureRvalue();          // error
const T& f = RetureRvalue();    // ok

在 C++98 中,常量左值引用就是个"万能"的引用类型.可以接受非常量左值、常量左值、右值对其进行初始化.而且使用右值对其初始化的时候,常量左值引用还可以向右值引用一样将右值的生命周期延长.不过相比于右值引用所引用的右值,常量左值引用所引用的右值在它的"余生"中只能是只读的.相对的,非常量左值引用只能接受非常量左值对其初始化.

在 C++98 中使用 常量左值引用 来减少临时对象的开销:

#include <iostream>
using namespace std;

struct Copyable {
    Copyable() {}
    Copyable(const Copyable &o) {
        cout << "Copied" << endl;
    }
};

Copyable ReturnRvalue() { return Copyable(); }
void AcceptVal(Copyable) {}
void AcceptRef(const Copyable & cp) {}//Copyable c = std::move(cp);}
void AcceptRRef(int && i) {i+=3; cout << (char)i << endl;}

int main() {
    cout << "Pass by value: " << endl;
    AcceptVal(ReturnRvalue()); // 临时值被拷贝传入
    cout << "Pass by reference: " << endl;
    AcceptRef(ReturnRvalue()); // 临时值被作为引用传递
    AcceptRRef('c'); // 临时值被作为引用传递
}

常量右值引用

const T&& crvalueref = RetureRvalue();

右值引用就是为了移动语义,而移动语义需要右值是可以被修改的,那么常量右值引用在移动语义中就没有用处;二来如果要引用右值且让右值不可以更改,常量左值引用就够了。

引用类型 非常量左值 常量左值 非常量右值 常量右值 注记
非常量左值引用 Y N N N
常量左值引用 Y Y Y Y 全部类型,可用于拷贝语义
非常量右值引用 N N Y N 用于移动语义、完美转发
常量右值引用 N N Y Y 暂无用途
#include <type_traits>

std::cout << is_lvalue_reference<string &&>::value;
std::cout << is_rvalue_reference<string &&>::value;

移动语义

#include <iostream>
using namespace std;

class A {
public:
    int x;
    A(int x) : x(x)
    {
        cout << "Constructor" << endl;
    }
    A(A& a) : x(a.x)
    {
        cout << "Copy Constructor" << endl;
    }
    A& operator=(A& a)
    {
        x = a.x;
        cout << "Copy Assignment operator" << endl;
        return *this;
    }
    A(A&& a) : x(a.x)
    {
        cout << "Move Constructor" << endl;
    }
    A& operator=(A&& a)
    {
        x = a.x;
        cout << "Move Assignment operator" << endl;
        return *this;
    }
};

A GetA()
{
    return A(1);
}

A&& MoveA()
{
    return A(1);
}

int main()
{
    cout << "-------------------------1-------------------------" << endl;
    A a(1);
    cout << "-------------------------2-------------------------" << endl;
    A b = a;
    cout << "-------------------------3-------------------------" << endl;
    A c(a);
    cout << "-------------------------4-------------------------" << endl;
    b = a;
    cout << "-------------------------5-------------------------" << endl;
    A d = A(1);
    cout << "-------------------------6-------------------------" << endl;
    A e = std::move(a);
    cout << "-------------------------7-------------------------" << endl;
    A f = GetA();
    cout << "-------------------------8-------------------------" << endl;
    A&& g = MoveA();
    cout << "-------------------------9-------------------------" << endl;
    d = A(1);
}
/*
-------------------------1-------------------------
Constructor
-------------------------2-------------------------
Copy Constructor
-------------------------3-------------------------
Copy Constructor
-------------------------4-------------------------
Copy Assignment operator
-------------------------5-------------------------
Constructor
-------------------------6-------------------------
Move Constructor
-------------------------7-------------------------
Constructor
-------------------------8-------------------------
Constructor
-------------------------9-------------------------
Constructor
Move Assignment operator
*/
#include <iostream>
using namespace std;

class HasPtrMem {
public:
    HasPtrMem(): d(new int(0)) {
        cout << "Construct: " << ++n_cstr << endl;
    }
    HasPtrMem(const HasPtrMem & h): d(new int(*h.d)) {
        cout << "Copy construct: " << ++n_cptr << endl;
    }
    ~HasPtrMem() {
        delete d;
        cout << "Destruct: " << ++n_dstr << endl;
    }
    int * d;
    static int n_cstr;
    static int n_dstr;
    static int n_cptr;
};

int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;

HasPtrMem GetTemp() { return HasPtrMem(); }

int main() {
    HasPtrMem a = GetTemp();
}
/*
 * 程序输出:
 * Construct: 1
 * Copy construct: 1
 * Destruct: 1
 * Copy construct: 2
 * Destruct: 2
 * Destruct: 3
*/
/*
 * 在新的版本的编译器程序输出:
 * Construct: 1
 * Destruct: 1
*/

本示例想说明一个问题,拷贝构造的调用,尤其是深拷贝构造的调用,会进行内存的memcpy,消耗大量资源。

C++11 的移动语义(move semantics):

不会进行拷贝构造,只是将临时变量“偷来”。

#include <iostream>
using namespace std;

class HasPtrMem {
public:
    HasPtrMem(): d(new int(3)) {
        cout << "Construct: " << ++n_cstr << endl;
    }
    HasPtrMem(const HasPtrMem& h) : d(new int(*h.d)) {
        cout << "Copy construct: " << ++n_cptr << endl;
    }
    HasPtrMem(HasPtrMem && h): d(h.d) { // 移动构造函数
        h.d = nullptr;                  // 将临时值的指针成员置空
        cout << "Move construct: " << ++n_mvtr << endl;
    }
    ~HasPtrMem() {
        delete d;
        cout << "Destruct: " << ++n_dstr << endl;
    }
    int * d;
    static int n_cstr;
    static int n_dstr;
    static int n_cptr;
    static int n_mvtr;
};

int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;
int HasPtrMem::n_mvtr = 0;

const HasPtrMem GetTemp() {
    HasPtrMem h;
    cout << "Resource from " <<  __func__ << ": " << hex << h.d << endl;
    return h;
}

int main() {
    const HasPtrMem && a = GetTemp();
    cout << "Resource from " <<  __func__ << ": " << hex << a.d << endl;
    // hex << a.d ===> hex(a.d)
}
/*
    Construct: 1
    Resource from GetTemp: 0x907ab0
    Resource from main: 0x907ab0
    Destruct: 1
*/
// 移动构造并没有被调用,该示例无法说明任何问题.

std::move 强制转换为右值

std::move 将一个左值强制转换为右值引用,继而我们通过右值使用该值,以用于移动语义。

// 等价于:
static_cast<T&&>(lvalue);

被强制转化的左值,其生命周期并没有随着左右值的变化而改变。

#include <iostream>

class Moveable {
public:
    Moveable(): i (new int(3)) {
        std::cout << "Moveable" << std::endl;
    }
    ~Moveable() { delete i; }
    Moveable(const Moveable & m) : i(new int(*m.i)) {}
    Moveable(Moveable && m) : i (m.i) {
        m.i = nullptr;
    }

    int *i;
};

int main()
{
    Moveable a;
    Moveable c(std::move(a));   // a 为左值,强制转换为右值
    std::cout << *(a.i) << std::endl; // 在 std::move(a) 时, a.i 就被设置为了 nullptr, 故这里运行时错误

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

推荐阅读更多精彩内容

  • 世有一职业:画妖师。只要出得起价钱,什么妖怪都能画出来。但画妖师也是分不同等级的,等级越高,画出的妖...
    骨头兔子阅读 427评论 0 2
  • 王菲的《清风徐来》一开始红遍网络的时候,我并不是很感冒,听了一遍,没听完,就关了 然后,就看到各大音乐网站排行榜上...
    彼岸青园阅读 251评论 0 1
  • 英国证券交易所前主管古迪逊曾经提出: 别做累坏的主管,管理是让别人工作的艺术。 现在很多培训机构的校长主管们觉得很...
    海鸥老师阅读 666评论 0 1