@网络老鼠技术小屋

网络老鼠技术小屋-涂飞平的博客空间

也说说C++11的右值引用

2 月前 0

最近看一个区块链项目代码,项目是采用C++14标准写的。我对于C++的理解和使用,一直停留在C++98阶段。由于在就职的两家公司里,除了用C++开发了一个基于CEF的项目,就没有关注过C++技术及后续的发展(所谓现代C++),这次看C++14的代码,感觉变化很大,完全可以认为是一个新的语言了,而最大发现当属右值引用(&&)了。
1、左值和右值
左值和右值在实际使用的时候大家是心知肚明的,赋值语句是大多数编程语言最基本要素。最简单的定义左值和右值:赋值语句赋值符号(=或者:=)左边的就是左值,赋值符号右边的就是右值。赋值语句将右边的值赋值给左边的变量。

int i = 5; // OK
int i = i + 2; // OK
5=5; // ERROR
int ret = MessageBox(0, "Message", "Caption", MB_OK); // OK
对常用编程语言熟悉人,上述语句肯定了然于胸,经验上会保证不会写出第三行那种错误代码。但其实并没有上升到一个左右值概念上来考量!
简单判断方法:如果可以应用取地址符的可以作为左值,如上面代码的i变量是可以通过&i获取变量地址,但不能通过&(i + 2)方式获取地址。
右值是不能应用取地址符(&)的,如果深入到汇编层面,左值是一个地址,取地址符一般映射到LEA指令,右值是字面值,表达式或者通过寄存器保存的临时值,比如函数是通过EAX寄存器保存返回值(如上面MessageBox函数调用)。
字面值(如上面的5),表达式(返回的也是放在寄存器里的临时值),寄存器是没有地址,取地址符对其没法使用。

2、左值引用
引用是C++等高级语言的一个概念,按照定义说:是一个变量的别名。
在C++的引用

  int i = 100;
int & j = i; // j 为i的别名
j = 1000; // 改变i的值
cout << i << endl;
Delphi也有引用
procedure ChangeStrVar(var A: string); 
begin
A := A + 'World'; // A就是别名,将直接改变传入的变量
end;
引用最大的作用就是在语义上给出对象本身,而不是一个对象指针,更好理解。
在对象访问的时候将箭头(->)符号改为点(.)作用符号,更简单明了。
using namespace std;
class A {
public:
void sayHello()
{
cout << "Hello" << endl;
}
};

A a = new A();
a->sayHello();
A& b = *a;
b.sayHello();

对于引用,C++/Delphi等语言在编译器编译的时候都是通过指针来实现的。
根据上面“左值和右值“部分说明的,只有左值能取地址。而这里说明,引用的底层是通过指针实现的,也就得出一个结论:只有左值能有引用!这也是C++98里面的结论,在C++98没有右值引用这个概念!
int global_i = 0;

int& getGlobal()
{
return global_i;
}

int main(int argc, char** argv)
{
cout << global_i << endl; // out 0
getGlobal() = 3;
cout << global_i << endl; // out 3
}

上面getGlobal() = 3;看似不合理,但该函数返回的是左值引用,是可以出现在赋值符号的左侧,编译通过并达到预期。

3、右值引用
首先,右值引用是新引入的概念,采用的表达式符号是&&,这个符号作为固定符号使用。
”左值和右值“部分说明过,右值都是临时值,生命周期在赋值完毕后可以认为结束了(EAX寄存器的值MOV到内存某个地方--变量)。所以,按理说右值引用是没有意义的。那为什么C++11开始会加上这个概念呢?
这要从C++的对象构建模式说起,Java和Delphi等高级语言对象都是建立在堆上面,通过对象指针来操作对象,并且函数参数传递对象或者返回对象,都是针对对象指针值。而C++的对象可以建立在栈上面,这就给对象的传递和返回对象造成了很多问题,如下所示:

#include 
#include
using namespace std;
const int ARRAYSIZE = 10;
class RVDemo
{
public:
explicit RVDemo() noexcept // 默认构造函数
{
mem = new int[ARRAYSIZE];
memset(mem, 0xFFFFFFFF, sizeof(int) * ARRAYSIZE);
cout << "+ default creator" << " mem is " << mem << " " << this << endl;
}

RVDemo(const RVDemo &src)
{
mem = new int[ARRAYSIZE];
memcpy(mem, src.mem, ARRAYSIZE);
cout << "+ copy creator " << &src << " to "<< this << endl;
}

~RVDemo()
{
cout << "- array list will be destroy " << mem <<
" " << this << " destroy" << endl;
delete[] mem;
}

RVDemo& operator=(const RVDemo& src)
{
if (&src == this)
{
return *this;
}
delete[] mem;
mem = new int[ARRAYSIZE];
memcpy(mem, src.mem, ARRAYSIZE);
cout << "* operator = " << endl;
return *this;
}

void print()
{

for (int i = 0; i < ARRAYSIZE; i++)
{
cout << i << " -> " << mem[i] << endl;
}
}

private:
int *mem;
};

RVDemo getRVDemo()
{
RVDemo demo; // 在栈中创建对象
return demo; // 返回对象并析构栈对象
}

int main(int argc, char *argv[])
{
RVDemo demo = getRVDemo(); // copy
demo.print();
}

为了得到正常结果,需要关闭C++编译器优化(C++编译器优化会合并减少过程以便提高效率),观察正常的输出。
-std=c++14 -fno-elide-constructors
20181117005252.png
可以看到上面代码的运行结果,构建一个对象并通过函数传递出来,整个过程总计发生3次构建和析构过程。函数getRVDemo内部在栈中创建了一个对象,然后在函数返回的时候,栈收回,会导致对象析构(生命周期结束),为了保证函数语义的正确性,C++编译器会在函数返回前生成临时对象框架(并不调用对象构造函数),并调用对象的拷贝构造函数,将右值(函数)返回对象的内容拷贝过来,析构栈中对象,再将临时对象通过同样方式复制到外部对象。
整个过程如下:
函数内部demo -copy-> 临时对象(匿名)-copy-> demo对象
如果是比较复杂的对象,比如上面代码从堆中申请内存,不断申请,释放,再申请,如果申请资源比较大,会导致运行效率低下。其实堆内存申请一次之后,上面3个过程可以一直传递下去,这样就不需要每次都进行内存的分配和释放了,无疑可以大大提高效率(对象框架复制对于效率的影响是很小的)。
C++11,加入右值引用来持有右值,同时在构造函数、拷贝构造函数,赋值函数之外加入了移动构造函数,如果对象定义了移动构造函数,C++编译器将优先使用移动构造函数(不再使用拷贝构造函数)。
  RVDemo(RVDemo &&rvh) : mem(rvh.mem) // 直接移动堆内存地址
{
cout << "right value copied [" << &rvh << "]" << endl;
cout << "mem is " << mem << endl;
rvh.mem = nullptr; // 将即将释放的右值对象堆地址设置为null,避免其析构函数释放移动到新对象的内存
}
移动构造函数与拷贝构造函数的区别如下图:
2018-11-17_02-14-55.png
加入这段代码后,运行的结果如下:
20181117013726.png
虽然还是3次对象的destroy,但mem的地址一直传递下去,并没有发生多次堆内存的申请和释放。
注意两点:
1、代码中RVDemo &&rvh 就是持有的右值引用,通过上面运行的结果,可以知道右值引用还是有临时的地址的(所以才能有引用);
2、右值引用完成操作后就不能再使用了,通过代码可以看到,移动构造函数会将右值引用对象的mem设置为null,所以这个对象的状态不再有效了。

上例是由C++编译器隐式完成右值引用到移动构造函数调用,也可以通过std::move来明确完成右值引用转换和使用。

  RVDemo demo1;
RVDemo demo = std::move(demo1);
demo.print();
// demo1.print(); //不能再使用了
20181117015820.png

右值引用扩充了引用概念的范围,提供了比之前(拷贝构造方式)更加高效的对象传递方式。

编写评论