std::move()实际应用分析

最近因为项目原因接触到了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

2 Comments

 Add your comment
  1. 请问函数名是左值还是右值啊?

Leave a Comment

Your email address will not be published.