C++11的feature应该不能算new feature了叭,毕竟这个版本已经年代久远了。来,我们学习一下C++11的右值引用。

什么是左值,什么是右值?

lvalue这个词来自于C语言,指的是可以放在赋值表达式左边的事物——在栈上或堆上分配的命名对象,或者其他对象成员——有明确的内存地址。

rvalue这个词也来源于C语言,指的是可以出现在赋值表达式右侧的对象——例如,文字常量和临时变量。

左值引用

首先我们回顾一下年代更为久远的左值引用。

1
2
3
4
int var=42;
int& ref=var; // 创建一个var的引用
ref=99;
assert(var==99); // 原型的值被改变了,因为引用被赋值了

左值引用只能被绑定在左值上,而不是右值。 因此左值引用不能这样子写:

1
int& i=42;  // 编译失败

不过我们可以钻空子。像这样:

1
int const& i = 42;

右值引用

C++11标准介绍了_右值引用_(rvalue reference),这种方式只能绑定右值,不能绑定左值,其通过两个&&来进行声明:

1
int&& i=42; // 正确

不能绑定左值噢:

1
2
int j=42;
int&& k=j; // 编译失败

右值引用用途の移动语义

右值通常都是临时的,所以可以随意修改;如果知道函数的某个参数是一个右值,就可以将其看作为一个临时存储或“窃取”内容,也不影响程序的正确性。这就意味着,比起拷贝右值参数的内容,不如移动其内容。动态数组比较大的时候,这样能节省很多内存分配,提供更多的优化空间。

精简版:在传参的时候使用右值引用可以避免在内存中创建重复的变量副本,空间复杂度更低。

举个例子叭,比如老的这种写法就很耗内存:

1
2
3
4
5
void process_copy(std::vector<int> const& vec_)
{
std::vector<int> vec(vec_);
vec.push_back(42);
}

在以上代码中,一个函数以std::vector<int>作为一个参数,就需要将其拷贝进来,而不对原始的数据做任何操作。

如果使用右值引用版本的函数来重载这个函数,就能避免在传入右值的时候,函数会进行内部拷贝的过程:

1
2
3
4
void process_copy(std::vector<int> && vec)
{
vec.push_back(42);
}

右值引用用途の 函数模板

如果函数模板参数以右值引用作为一个模板参数,当对应位置提供左值的时候,模板会自动将其类型认定为左值引用;当提供右值的时候,会当做普通数据使用。

我帮你整理一下思路:

1
2
3
4
5
6
7
8
if(函数模板参数以右值引用作为一个模板参数){
if(对应位置提供左值){
模板会自动将其类型认定为左值引用
}
if(对应位置提供右值){
会当做普通数据使用
}
}

举个栗子,定义一个函数模板:

1
2
3
template<typename T>
void foo(T&& t)
{}

随后传入一个右值,T的类型将被推导为:

1
2
3
foo(42);  // foo<int>(42)
foo(3.14159); // foo<double><3.14159>
foo(std::string()); // foo<std::string>(std::string())

不过,向foo传入左值的时候,T会被推导为一个左值引用:

1
2
int i = 42;
foo(i); // foo<int&>(i)

因为函数参数声明为T&&,所以就是引用的引用,可以视为是原始的引用类型。那么foo()就相当于:

1
foo<int&>(); // void foo<int&>(int& t); 

这就允许一个函数模板既可以即接受左值,又可以接受右值参数。

小结

我萌先回顾了左值、右值的术语概念和左值引用的语法,然后介绍了右值引用的语法,最后讲了右值引用的两种用途:移动语义和函数模板。

加油啦,你最棒!

本文采用CC-BY-SA-3.0协议,转载请注明出处
Author: 樱花雨