现代C++之”左右值与移动语义”

从17年开始就不太做C++开发了,因此知识还停留在C++03上,所以这次趁着过年的短暂空隙补一下比较有意思的现代C++特性,有些简单的新语法不是我想表达的重点就一笔带过了。

本文通过1个简单例子来了解”左右值”,代码:https://github.com/owenliang/modern-cpp/blob/main/lrvalue.cpp

下面是C++03时代的主流写法:

通过const &可以避免参数发生拷贝构造,但是copy对象还是通过拷贝产生的,付出了性能代价。

然后我们可以这样调用它:

这里v被定义出来,可以取到它的地址&v,所以就是个”左值”,test(v)可以传const &进去,函数内无法修改我们的v,这就是C++03普通写法。

test(std::vector<int>{1,2,3});的参数是一个临时对象,我们没法在这里&std::vector<int>{1,2,3}取临时对象的地址吧?所以这种无法取值的对象就是”右值”的一种典型例子。

但是没关系啊,临时对象还是可以被const &参数接收,等函数返回后临时对象就被销毁了,这也是C++03正常写法,没什么意外,打印如下:

const std::vector<int>&, copy size:3, lvalue ref size:3
const std::vector<int>&, copy size:3, lvalue ref size:3

现在我们看看现代C++带来了什么改变~~~

现代C++提出了一个”右值的移动语义”,既然test返回后临时对象就被销毁了,同时test内部为了保留住临时对象还copy了一份,那能不能有一种方式可以把这个临时对象的数据直接”移动”给copy对象而不是拷贝一份?如果”移动”可以的话,我们只需要把临时vector内部的int*数组直接赋值给copy对象,然后让临时vector内部的int*为NULL,这样就完成了”移动”,就避免了拷贝int*数组的代价,简直躺赢嘛。

重载一个test函数接受右值引用:

这样的话test(std::vector<int>{1,2,3});就会自动走这个重载版本,然后怎么偷临时vec的int*呢?

首先你得明白,虽然调用test(std::vector<int>{1,2,3})的时候vector是个临时对象导致我们无法取到地址,但是进入test函数内就可以取vec地址了(&vec),这很好理解啊,在调用时的确没法表达取值啊,现在内部的确可以写&vec了,因此在函数内vec实际已经是个左值了,我是很佩服c++标准委员会的这些神奇想法的啊,哈哈。

既然vec已经是左值了,那么如果直接创建vector steal对象的话,它会走vector(const vector&)的版本,又变成拷贝int*了,所以怎么办呢?把vec再std::move一下,也就是手动让它再变成”右值”,实际上move就是static_cast<vector<int>&&>,然后就会走vector(vector&&)的移动构造函数版本,把vec对象的int*偷走并且赋值NULL,这样就”移动”完成了。

所以现在我们执行代码,就会看到输出有变化了:

const std::vector<int>&, copy size:3, lvalue ref size:3
in fact, vec is lvalue now, 0x7ffeea03c800
std::vector<int>&&, steal size:3, rvalue size:0

第1行是test(v)走的const &版本重载;第2~3行就是&&版本重载,可以看到函数内vec是个”左值”(能取地址呗),然后”移动构造”steal之后steal长度3而vec内部size=0了,也就是偷走了。

最后再说明一个事实,就是我们可以主动让一个”左值”转成”右值”,然后就可以走”右值”版本的重载函数完成”移动”,这个就看大家需求了,下面最后一个调用演示了这种情况:

test(std::move(v));就是让v变成右值属性,然后就走test(vector&&)版本,最终导致v里面的int*被偷走了,成为了一个空v,对应日志:

in fact, vec is lvalue now, 0x7ffeea03c838
std::vector<int>&&, steal size:3, rvalue size:0

 

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~