在 C++11 的新特性中增加了所谓的右值引用的支持,其主要目的是为了解决两个问题:A) 临时对象非必要的拷贝操作;B) 在模板函数中如何按照参数的实际类型进行转发。
同时,和右值引用相关的概念比较多,包括了纯右值、将亡值、Universal References、引用折叠、移动语义、Move语义和完美转发等等,这里简单介绍。
引用介绍
在介绍之前,先看下 C++ 中最原始引用变量的定义。引用变量可以看做是另外一个变量的别名,可以通过引用来访问和修改原变量,而这,严格来说是左值引用。
引用与指针类似,很多地方通过引用替换指针会更容易阅读和维护,当然,两者使用方式和场景略有区别:
- 不存在空引用,在声明时必须初始化,需要指向一块合法内存;指针可以为空,允许后续指定。
- 一旦指定引用对象,不能修改;指针可以随时指向另外的对象。
如下是一个简单示例。
#include <iostream>
int main(void)
{
int var;
int &ref = var;
int *ptr;
/* ERROR: declared as reference but not initialized
int &ref;
ref = var;
*/
var = 10;
ptr = &var;
std::cout << "Value " << var << ", Reference " << ref << ", Pointer " << *ptr << std::endl;
}
其输出为。
Value 10, Reference 10, Pointer 10
除了对变量的使用,最常见的是在函数参数以及函数返回值中使用。
函数参数
传递参数时包含了值传递、指针传递、引用传递三种方式,其中值传递会将原变量复制一份,会导致函数中的修改无法影响到原变量,后两者则可以影响,可以根据场景使用。
如下是一个简单的交换值的示例。
#include <iostream>
void swap(int &x, int &y)
{
int tmp;
tmp = x;
x = y;
y = tmp;
}
int main(void)
{
int x = 1, y = 2;
std::cout << "Before: x=" << x << ", y=" << y << std::endl;
swap(x, y);
std::cout << "After : x=" << x << ", y=" << y << std::endl;
}
该函数的执行结果为。
Before: x=1, y=2
After : x=2, y=1
可以看到两个值已经交换,如果将函数定义为 void swap(int x, int y)
,实际上是不会交换的。
返回值
当引用作为返回值时,类似于返回一个隐式指针,这样,除了获取该元素外,甚至可以将其放到赋值语句的左边,也就是所谓的左值。
#include <iostream>
int vals[] = {1, 2, 3, 4, 5};
int &GetValue(int idx)
{
return vals[idx];
}
int main(void)
{
std::cout << "vals[1]=" << GetValue(1) << std::endl;
std::cout << "Before:";
for (int i = 0; i < 5; i++)
std::cout << " vals[" << i << "]=" << vals[i];
std::cout << std::endl;
GetValue(1) = 4;
GetValue(3) = 8;
std::cout << "After :";
for (int i = 0; i < 5; i++)
std::cout << " vals[" << i << "]=" << vals[i];
std::cout << std::endl;
}
其输出结果如下。
vals[1]=2
Before: vals[0]=1 vals[1]=2 vals[2]=3 vals[3]=4 vals[4]=5
After : vals[0]=1 vals[1]=4 vals[2]=3 vals[3]=8 vals[4]=5
也就是,通过左值完成了原数组元素的修改。
另外,需要注意,当返回一个引用时,需要注意被引用对象的作用域,返回一个局部变量的引用是不合法的,但允许返回一个对静态变量的引用。
#include <iostream>
int &FooBar(void)
{
/* warning: reference to local variable ‘bar’ returned [-Wreturn-local-addr]
int bar = 10;
return bar;
*/
static int foo = 10;
return foo;
}
int main(void)
{
std::cout << "FooBar " << FooBar() << std::endl;
}
返回局部变量时会有一个 warning 信息,如果栈被销毁后被覆盖,那么指向的值就会被修改,所以,严格来说不能这么使用。
左值 VS. 右值
最开始实际很少会区分所谓的左值(lvalue)、右值(rvalue),只是在 C/C++11 版本之后,在做一些性能优化时引入了一些新的概念,也同时导致了这两者也很容易混淆。
首先,可以这么简单定义 (后面会补充细节),左值是可标识的内存位置,可以通过 &
符号取地址;而右值定义用的是排除法,一个表达式,不是左值就是右值。
示例
例如,当执行 int i = foobar();
语句时,通过函数获取一个整形值,但实际上返回了两种类型:A) 左值 i
会一直存在;B) 临时值,在表达式结束时被销毁,这个也就是右值。左值可以执行 &i
取其地址,但右值无法执行 &foobar()
获得地址。
另外一个示例是 int a = b + c;
,其中 a
就是左值,可以通过 &a
获取其地址,而表达 b + c
是右值,在赋值给某个具体的变量前,不能直接通过变量名获取,例如 &(b + c)
会直接报错。
另外,还有个区别,左值可以出现在赋值操作的左边或者右边,而右值只能出现在右边。
如下是一些常见的示例。
#include <iostream>
int global = 10;
int GetRValue(void)
{
return global;
}
int &GetLValue(void)
{
return global;
}
int main(void)
{
int var;
int *ptr;
var = 8; // OK var is a l-value, and 8 is a r-value
ptr = &var; // OK both ptr and var are l-value
//8 = var; // ERROR r-value on the left
//(var + 1) = 8; // ERROR r-value on the left
ptr = &GetLValue(); // OK return a l-value
GetLValue() = 20; // OK return a l-value
//ptr = &GetRValue(); // ERROR return a r-value
//GetRValue() = 20; // ERROR return a r-value
}
简单来说,常见的如常量 (如上的 8
)、临时值 (通过寄存器而非地址保存数据,如上的 var + 1
) 都会被看做右值,临时值也包括了函数返回的临时值。
上述的 GetLValue()
返回左值对一些重载运算符很有用,例如常见的 []
操作符重载,可以用来实现访问的查找操作,例如 std::map
中的使用:
std::map<int, float> amap;
amap[10] = 3.1415926;
之所以可以赋值给 amap[10]
,就是因为 std::map::operator[]
重载后返回的是一个可赋值的引用。
通用引用
当右值引用和模板结合之后,此时的 T&&
就不一定表示右值引用了,可能是左值也可能是右值,这取决于入参。
#include <iostream>
template<typename T>
void dump(T &&data) {
std::cout << data << std::endl;
}
int main() {
dump(10); // 这是一个右值
int val = 10;
dump(val); // 这是一个左值
}
正常是无法将左值赋值给右值引用的,这里却是可以的。实际上,这里的 &&
是一个未定义的引用类型,被称为 universal references
,必须被初始化才行,是左值还是右值取决于初始化的值,如果被左值初始化那么就是左值引用,反之亦然。
注意,只有当发生自动类型推断时,其 &&
才是通用引用,例如上述的函数模板,以及 auto
关键字等。
如下是更多的示例。
template<typename T>
void foobar(T &¶m); // T类型需要推导,所以 && 是通用引用
template<typename T>
class Test {
Test(Test &&rhs); // Test特定类型,不需要类型推导,所以&&表示右值引用
};
void foobar(Test &¶m); // 同上,右值引用
template<typename T>
void foobar(std::vector<T>&& param); // 调用前的vector<T>已经确定,所以该函数无需类型推断,是右值引用
template<typename T>
void foobar(const T&& param); // 右值引用
也就是说,通用引用仅发生在 T&&
这一场景下,任何一点附加条件都会使之失效。
引用折叠
所以,上述关键要看 T
被推导成什么类型,如果推导为 string
那么 T&&
就是 string&&
对应了右值引用;如果推导为 string&
那就是 string& &&
类型。这就对应了 C++11 中的引用折叠规则:
- 右值引用叠加到右值引用上仍然使一个右值引用。
- 其它引用类型之间的叠加都将变成左值引用。
那么上面的 string& &&
折叠后就是 string&
类型,也就是左值引用。
#include <string>
#include <iostream>
#include <type_traits>
template<typename T>
void foobar(T &¶m) {
if (std::is_same<std::string, T>::value)
std::cout << "string" << std::endl;
else if (std::is_same<std::string &, T>::value)
std::cout << "string&" << std::endl;
else if (std::is_same<std::string &&, T>::value)
std::cout << "string&&" << std::endl;
else if (std::is_same<int, T>::value)
std::cout << "int" << std::endl;
else if (std::is_same<int &, T>::value)
std::cout << "int&" << std::endl;
else if (std::is_same<int &&, T>::value)
std::cout << "int&&" << std::endl;
else
std::cout << "unkown" << std::endl;
}
int main() {
int x = 1;
foobar(1); // 参数是右值,T->int->int&& 右值引用
foobar(x); // 参数是左值,T->int&->int& && 左值引用
int &&a = 2;
foobar(a); // 虽然a是右值引用,但其本身还是左值,所以T推导成了int&
std::string str = "hello";
foobar(str); // 参数是左值,T推导成了string&
foobar(std::string("hello")); // 参数是右值,T推导成了string
foobar(std::move(str)); // 参数是右值,T推导成了string
}
简单来说,传递左值进去就是左值引用,传递右值进去就是右值引用,这就是其名字的由来,下面的完美转发就是利用了这个特性。
完美转发
转发就是通过一个函数将参数继续转交给另一个函数进行处理,原参数可能是右值或者左值,如果还能继续保持参数的原有特征,那么它就是完美的,就是所谓的完美转发。
#include <string>
#include <iostream>
void process(std::string &str) {
std::cout << "string& " << str << std::endl;
}
void process(std::string &&str) {
std::cout << "string&& " << str << std::endl;
}
void forward(std::string &&str) {
std::cout << "forward " << str << std::endl;
process(str);
}
int main() {
std::string a = "Hello";
forward(std::move(a)); // 右值因为有了名字,最后转发的实际上是左值
//forward(a); // 错误,右值不接受左值
}
上述就是不完美的转发,将一个右值经过转发后变成了左值,解决办法是将 forward()
函数修改为如下,利用标准库中的模板函数解决。
void forward(std::string &&str) {
std::cout << "forward " << str << std::endl;
process(std::forward<std::string>(str));
}
不过这里还是无法处理左值的,此时就需要借助通用引用类型和 std::forward()
函数模板共同实现,如下是一个简单示例。
#include <string>
#include <iostream>
void process(std::string &str) {
std::cout << "string& " << str << std::endl;
}
void process(const std::string &str) {
std::cout << "const string& " << str << std::endl;
}
void process(std::string &&str) {
std::cout << "string&& " << str << std::endl;
}
void process(const std::string &&str) {
std::cout << "const string&& " << str << std::endl;
}
template<typename T>
void forward(T &&str) {
process(std::forward<T>(str));
}
int main() {
std::string a = "Hello";
const std::string b = "World";
forward(a);
forward(std::move(a));
forward(b);
forward(std::move(b));
}
其它
数组
使用 std::vector
时一般会通过 push_back()
函数添加,如果实现了移动构造函数,那么可以通过 emplace_back()
赋值,这样可以有效减少数据的复制,尤其是大对象。