本文摘自博客:https://www.ibm.com/developerworks/cn/aix/library/1307_lisl_c11/index.html
1、新特性的目的
- 右值引用是C++11中引入的新特性,它实现了转移语义和精确传递
- 消除两个对象交互式不必要的对象拷贝,节省存储资源,提高效率;
2、左值和右值的定义
- 左值是非临时对象,可以取地址,可以被赋值;右值是临时对象,只在当前语句有效。不能被取地址和赋值。
- 如果右值出现在表达式的左边,那么他不能作为赋值的对象。如(i>0)?i:j = 1。
- 在C++11之前,右值不能被引用,最大限度就是用常量引用绑定一个右值,如const int &a=1。在这种情况下右值不能被修改。
3、左值和右值的语法符号
左值的申明符号为&,右值的申明符号为&&
//运行结果 //LValue processed: 0
//RValue processed: 1
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);
}
process_value函数被重载,分别接受左值和右值。由输出结果可以看出,临时对象被作为右值处理的。
但是如果临时对象通过一个接受右值的函数传递另一个参数时,就会变成左值,因为这个临时对象在传递过程中,变成了命名对象。
//运行结果
//LValue processed: 0
//RValue processed: 1
//LValue processed: 2
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)
{ process_value(i);
}
int main()
{
int a = 0;
process_value(a);
process_value(1);
forward_value(2);
}
虽然 2 这个立即数在函数 forward_value 接收时是右值,但到了 process_value 接收时,变成了左值。
4、转移语句的定义
- 右值引用是用来支持转移语义的。转移语义可以将资源(堆、系统对象等)从一个对象转移到另一个对象,这样能减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高性能,临时对象的维护(创建和销毁)对性能有严重影响。
- 通过转移语义,临时对象的资源可以转移到其他对象中
- 要实现转移语义,需要定义转移构造函数,还可以定义转移赋值操作符。对于右值的拷贝和复制会调用转移构造函数和转移赋值操作符。如果两个函数没有定义,那么就调用现有的拷贝构造函数和赋值操作符。
- 普通的函数和操作符也可以利用右值操作符实现转移语义。
以一个简单的 string 类为示例,实现拷贝构造函数和拷贝赋值操作符。
class MyString
{
private:
char* _data;
int _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)
{
cout<<"MyString(const &)"<<endl;
_len = str._len;
_init_data(str._data);
}
MyString& operator=(const MyString& str)
{
cout<<"operator=(const &)"<<endl;
if (this != &str)
{
_len = str._len;
_init_data(str._data);
}
return *this;
}
virtual ~MyString()
{
if (_data)
free(_data);
}
};
int main()
{
MyString a;
a = MyString("Hello");
std::vector<MyString> vec;
vec.push_back(MyString("World"));
}
运行结果:
在main()函数中,实现了调用赋值操作符的操作和拷贝构造函数。Mystring(“hello”)和MyString(“World”)都是临时对象,也就是右值。虽然它们是临时的,但是程序还是会调用拷贝构造函数和赋值操作符函数,造成了没有意义的资源申请和释放操作。定义转移语义的目的就是希望直接使用临时对象已经申请的资源,这样既能节省资源,又能节省资源申请和释放的时间。
定义转移构造函数
MyString(MyString&& str)
{
std::cout << "Move Constructor is called! source: " << str._data << std::endl;
_len = str._len;
_data = str._data;
str._len = 0;
str._data = NULL;
}
注意:
参数(右值)的符号必须是右值引用符号,“&&”
参数(右值)不可以是常量,因为我们需要修改右值
参数(右值)的资源必须修改,否则,右值的析构函数会释放资源,转移到新对象的资源就无效了。
定义转移赋值操作符
MyString& operator=(MyString&& str)
{
std::cout << "Move Assignment is called! source: " << str._data << std::endl;
if (this != &str)
{
_len = str._len;
_data = str._data;
str._len = 0;
str._data = NULL;
}
return *this;
}
运行结果:
由此可以看出,编译器区分了左值和右值,对右值调用了转移构造函数和转移赋值操作符,节省了资源,提高了程序运行效率。故我们在设计类时,如果需要动态申请大量资源,我们应该设计转移构造函数和转移赋值操作符,以提高效率。
5、move函数
编译器只对右值引用才调用转移构造函数和转移赋值函数,但是所用命名对象都是左值引用,如果想把一个左值引用当作右值引用来使用,就可以使用move函数将左值引用转换成右值引用。
//运行结果
//LValue processed: 0
//RValue processed: 0
void ProcessValue(int& i)
{
std::cout << "LValue processed: " << i << std::endl;
}
void ProcessValue(int&& i)
{
std::cout << "RValue processed: " << i << std::endl;
}
int main()
{
int a = 0;
ProcessValue(a);
ProcessValue(std::move(a));
}
move函数可以移动某资源到另一个对象,而不是拷贝一份后复制给新对象,就像剪切操作和复制操作一样。
6、精确传递
精确传递适用于这样的场景:需要将一组函数原封不动的传递给另一个函数。
原封不动指的是不仅参数的值不变,而且属性也不变(左值还是右值)
精确传递在泛型函数中,需求比较普遍。下面举一个例子,函数forward_value是一个泛型函数,它将一个参数传递给另一个函数process_value。
Forward_value的定义为:
template <typename T> void forward_value(const T& val)
{
process_value(val);
}
template <typename T> void forward_value(T& val)
{
process_value(val);
}
函数forward_value为每一个参数必须重载两种类型,T&和const T&,否则,下面四种不同类型参数的调用就不能同时满足:
int a = 0;
const int &b = 1;
forward_value(a); // int&
forward_value(b); // const int&
forward_value(2); // ----int&
对于一个参数就要重载两次,就是说函数重载和参数个数成正比,这样来说是非常低效的。看看右值引用如何解决
template <typename T> void forward_value(T&& val)
{
process_value(val);
}
只定义一次,接收一个右值引用的参数,就能将所有的参数类型原封不动的传递给目标函数。四种不同类型参数的调用都能满足,参数的左右值属性和const属性都完全传递给目标函数process_value。
int a = 0;
const int &b = 1;
forward_value(a); // int&
forward_value(b); // const int&
forward_value(2); // int&&
C++11中定义的T&&的推导规则为:右值实参为右值引用,左值实参仍然为左值引用。