最近因为项目原因接触到了C++11的移动语义(move-semantics),参考了大量资料。不少资料都是从C++左右值的概念到C++11的右值引用,再牵扯出构造函数的实现方式,最后给出一个“实际”意义不高的一串代码,到最后也看不出这个新加入的语义是为了解决什么问题。
于是想自己记录一篇文章,用几句话来解释一些基本概念,几段代码来展示它我个人认为最直观的用途。
左值(lvalue)与右值(rvalue)
用一句话来概括形容就是:
左值:放在等号左边的值,可以被赋值;
右值:放在等号右边的值,不能被赋值;
比较典型的左值(以下各种运算符均是C++内建):
a
++a
*a
**a
a.m
a->m
a[m]
比较典型的右值(以下各种运算符均是C++内建):
a + b
a++
a && b
a < b
&a
深拷贝与浅拷贝
比如我们有A、B两个对象,里面均有指针指向对应内容,现在A要对B进行拷贝:
深拷贝:A先申请出一片新的空间,完全复制B的内容到新空间中;
浅拷贝:A复制B指针,将自己的指针指向B的内容地址,A、B公用一块内存地址;
所以对于深拷贝,A的修改与B没有关系;对于浅拷贝,A的修改也会在B对象也会被改变。
左值引用与右值引用
左值引用:传统的引用,形如T&;
右值引用:C++11新的数据类型,为了实现移动语义与完美转发所需要而设计出来的新的数据类型,形如T&&;
std::move()使用的意义
协助使用者进行浅拷贝,前提条件是拷贝对象需要支持移动赋值(move-assignment)、移动构造(move-constructor)。
一个具体的例子,在没有使用std::move()之前:
//test.cpp #include <map> #include <stdio.h> int main() { std::map<int, int> mapA; mapA[1] = 1; mapA[2] = 2; mapA[3] = 3; printf("addr A: %x|%x|%x\n", &mapA[1], &mapA[2], &mapA[3]); std::map<int, int> mapB = mapA; printf("addr B: %x|%x|%x\n", &mapB[1], &mapB[2], &mapB[3]); mapB[4] = 4; printf("addr new: %d|%d\n", mapA[4], mapB[4]); return 0; }
$g++ -std=c++11 -o test test.cpp $./test addr A: 1646034|1646064|1646094 addr B: 1646124|16460c4|16460f4 addr new: 0|4
在使用了std::move()之后:
//test.cpp #include <map> #include <stdio.h> int main() { std::map<int, int> mapA; mapA[1] = 1; mapA[2] = 2; mapA[3] = 3; printf("addr A: %x|%x|%x\n", &mapA[1], &mapA[2], &mapA[3]); std::map<int, int> mapB = std::move(mapA); printf("addr B: %x|%x|%x\n", &mapB[1], &mapB[2], &mapB[3]); mapB[4] = 4; printf("addr new: %d|%d\n", mapA[4], mapB[4]); return 0; }
$g++ -std=c++11 -o test test.cpp $./test addr A: 2017034|2017064|2017094 addr B: 2017034|2017064|2017094 addr new: 0|4
看到在使用std::move之后,B对A进行了浅拷贝,仅仅只赋值了key=1,2,3的指针。那么这里引发了一个新的问题:在move(mapA)之后,我们并不希望再进一步对A中key=1,2,3的对象做操作,否则会引起“不可预期”的结果,比如释放了同一个地址。所以我们需要约束对于move后的对象,应当马上“弃用”(std::map已经自动做了这个操作,在move之后A里面的key全部被清空了)。
这里可能会有人问,为什么要搞这么麻烦,我直接用C++传统的左值引用不就可以了吗?代码如下
//test.cpp #include <map> #include <stdio.h> int main() { std::map<int, int> mapA; mapA[1] = 1; mapA[2] = 2; mapA[3] = 3; printf("addr A: %x|%x|%x\n", &mapA[1], &mapA[2], &mapA[3]); std::map<int, int> &mapB = mapA; printf("addr B: %x|%x|%x\n", &mapB[1], &mapB[2], &mapB[3]); mapB[4] = 4; printf("addr new: %d|%d\n", mapA[4], mapB[4]); return 0; }
$g++ -std=c++11 -o test test.cpp $./test addr A: 25d7034|25d7064|25d7094 addr B: 25d7034|25d7064|25d7094 addr new: 4|4
通过输出结果可以很明显的看出,传统的左值引用导致A,B互相影响,就语境来说不太利于把A拷贝到B,然后丢弃A的场景。
所以说移动语义补充了这块空白,其核心就是利用浅拷贝来模拟对象的移动行为,提高了效率,让语义更加明确。
(全文结束)
转载文章请注明出处:漫漫路 - lanindex.com
请问函数名是左值还是右值啊?
在C++中是左值,可以返回值取地址;
在C中只能是右值。