右值引用和转移语义
本文尝试着解释何为右值引用和转移语义以及使用它们具有优势,并提供相关案例分析。
定义
左值和右值
首先我们先来理解一下什么是左值和右值。
C/C++语言中可以放在赋值符号左边的变量,左值表示存储在计算机内存的对象,左值相当于地址值。右值:当一个符号或者常量放在操作符右边的时候,计算机就读取他们的“右值”,也就是其代表的真实值,右值相当于数据值。
C/C++语言中可以放在赋值符号左边的变量,即具有对应的可以由用户访问的存储单元,并且能够由用户去改变其值的量。左值表示存储在计算机内存的对象,而不是常量或计算的结果。或者说左值是代表一个内存地址值,并且通过这个内存地址,就可以对内存进行读并且写(主要是能写)操作;这也就是为什么左值可以被赋值的原因了。相对应的还有右值:当一个符号或者常量放在操作符右边的时候,计算机就读取他们的“右值”,也就是其代表的真实值。简单来说就是,左值相当于地址值,右值相当于数据值。右值指的是引用了一个存储在某个内存地址里的数据。
左值右值翻译:
L-value中的L指的是Location,表示可寻址。A value (computer science)that has an address.
R-value中的R指的是Read,表示可读。in computer science, a value that does not have an address in a computer language.
左值和右值是相对于赋值表达式而言的。左值是能出现在赋值表达式左边的表达式。左值表达式可以分为可读写的左值和只读左值。右值是可以出现在赋值表达式右边的表达式,他可以是不占据内存空间的临时量或字面量,可以是不具有写入权的空间实体。如
int a=3;
const int b=5;
a=b+2; //a是左值,b+2是右值
b=a+2; //错!b是只读的左值但无写入权,不能出现在赋值符号左边
(a=4)+=28; //a=4是左值表达式,28是右值,+=为赋值操作符
34=a+2; //错!34是字面量不能做左值
(from 百度百科)
左值引用
左值引用根据其修饰符的不同,可以区分为常量左值引用和非常量左值引用。左值引用实际上就是指针。
非常量左值引用只能绑定到非常量左值,不能绑定到常量左值和常量右值,(因为非常左值可以改变其值,但常量不可改变,性质相矛盾),非常量右值。而如果绑定到非常量右值,就有可能指向一个已经被销毁的对象。
常量左值引用能绑定到非常量左值,常量左值,非常量右值,常量右值。
右值引用
从实践角度讲,它能够完美解决C++中长久以来为人所诟病的临时对象效率问题。从语言本身讲,它健全了C++中的引用类型在左值右值方面的缺陷。从库设计者的角度讲,它给库设计者又带来了一把利器。从库使用者的角度讲,不动一兵一卒便可以获得“免费的”效率提升…
右值引用 (Rvalue Referene) 是 C++ 新标准 (C++11, 11 代表 2011 年 ) 中引入的新特性 , 它实现了转移语义 (Move Sementics) 和精确传递 (Perfect Forwarding)。它的主要目的有两个方面:
消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
能够更简洁明确地定义泛型函数。
左值引用和右值引用的语法
为了区别,C++把&作为左值引用的声明符,把&&作为右值引用的声明符。
void process_value(int& i) {
std::cout << "LValue processed: " << i << std::endl;
}
void process_value(int&& i) {
std::cout << "RValue processed: " << i << std::endl;
}
int main() {
int a = 0;
process_value(a);
process_value(1); // 1对于编译器而言就是临时对象。
}
output:
LValue processed: 0
RValue processed: 1
Program ended with exit code: 0
但是如果临时对象通过一个接受右值的函数传递给另一个函数时,就会变成左值,因为这个临时对象在传递过程中,变成了命名对象。
void process_value(int& i) {
std::cout << "LValue processed: " << i << std::endl;
}
void process_value(int&& i) {
std::cout << "RValue processed: " << i << std::endl;
}
void forward_value(int&& i) {
// 在函数传递中i被认为是命名对象。
process_value(i);
}
int main() {
int a = 0;
process_value(a);
process_value(1);
forward_value(2);
}
output:
LValue processed: 0
RValue processed: 1
LValue processed: 2
Program ended with exit code: 0
转移语义
右值引用是用来支持转移语义的。转移语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。
转移语义是和拷贝语义相对的,可以类比文件的剪切与拷贝,当我们将文件从一个目录拷贝到另一个目录时,速度比剪切慢很多。
通过转移语义,临时对象中的资源能够转移其它的对象里。(注意是临时对象中的“资源”而不是临时对象本身!这里所谓的使用资源是指指针的指向问题,通过改变指针的指向可以直接使用临时对象中的资源。所以如果,临时对象中并不涉及动态分配内存的问题时,转移语义并不能起到作用,也不必起作用。)
在现有的 C++ 机制中,我们可以定义拷贝构造函数和赋值函数。要实现转移语义,需要定义转移构造函数,还可以定义转移赋值操作符。对于右值的拷贝和赋值会调用转移构造函数和转移赋值操作符。如果转移构造函数和转移拷贝操作符没有定义,那么就遵循现有的机制,拷贝构造函数和赋值操作符会被调用。
普通的函数和操作符也可以利用右值引用操作符实现转移语义。
实例:实现转移构造函数和转移赋值函数
以一个简单的 string 类为示例,实现拷贝构造函数和拷贝赋值操作符。
示例1:没有转移构造函数和转移copying函数
class MyString {
private:
char* _data;
size_t _len;
void _init_data(const char *s) {
_data = new char[_len+1];
memcpy(_data, s, _len);
_data[_len] = '\0';
}
public:
MyString() {
_data = NULL;
_len = 0;
}
MyString(const char* p) {
_len = strlen (p);
_init_data(p);
}
MyString(const MyString& str) {
_len = str._len;
_init_data(str._data);
std::cout << "Copy Constructor is called! source: " << str._data << std::endl;
}
MyString& operator=(const MyString& str) {
if (this != &str) {
_len = str._len;
_init_data(str._data);
}
std::cout << "Copy Assignment is called! source: " << str._data << std::endl;
return *this;
}
virtual ~MyString() {
if (_data) free(_data);
}
};
int main() {
MyString a;
a = MyString("Hello");
std::vector
vec; vec.push_back(MyString("World")); }
output:
Copy Assignment is called! source: Hello
Copy Constructor is called! source: World
这个类基本满足我们的需求。但是实际上他的效率很低。因为每一次赋值操作符的调用都会先析构原有的对象的内存,临时对象构造,复制,析构等一系列操作。拷贝构造函数也是如此。非常地低效。于是有人想到,如果能够直接使用临时对象已经申请的资源,既能节省资源,有能节省资源申请和释放的时间。这正是定义转移语义的目的。
示例2:实现转移构造函数和转移copying函数
class MyString {
private:
char* _data;
size_t _len;
void _init_data(const char *s) {
_data = new char[_len+1];
memcpy(_data, s, _len);
_dat