右值引用
C++中的值类别(Value Category)
一般而言,C++中将值分为两种类别:左值(lvalue)和右值(rvalue)。有一种细的分类方式,将其分成了三类:lvalue,xvalue,prvalue。这里的xvalue指的是,lvalue转的rvalue和rvalue转的lvalue,prvalue是指纯右值,逻辑上存在,可能会被编译器优化的rvalue,这个后面再详细说。先重点说lvalue和rvalue:
C++中的部分概念很难定义,手册中也都是以示例的方式给出。下面的定义,也是只是我自己的理解。
- lvalue:有读写权限的内存地址
- rvalue:只有可读权限的内存地址,或者说真实存在,但我们不可见的内存地址
lvalue的例子
std::string str("123"); // 可读可写
int i = 5;
rvalue的例子
字面值:42, true, nullptr; // 只能读
函数返回值:std::string fun(); // 如果不写成这样,std::string str = fun(); 我们都看不见这个内存地址
但是如果我们想操作rvalue怎么办,C++11新增的std::move可以解决这个问题。首先为什么我们非得操作rvalue,因为我们想获取更多的性能上的好处。比如:
void fun(const std::string& str) {
// something
}
std::string long_string(); // 返回一个很长的std::string
fun(long_string());
将long_string()的返回值传递给fun的时候,会调用一次std::string的拷贝构造函数,由于这个string很长,性能开销很大。所以C++11就通过右值引用的方式来解决这个问题。
右值引用的例子
修改fun来支持右值引用
class Widget {
public:
void fun(std::string&& str) {
p_str_.swap(str);
}
private:
std::string p_str_;
}
std::string long_string(); // 返回一个很长的std::string
Widget widget;
widget.fun(long_string());
此时在fun中我们就可以直接操作long_string()返回的rvalue了,比如swap操作,效率就会高很多。
xvalue
lvalue和rvalue都可以转成xvalue,xvalue是可以随意破坏内存地址的一种特殊lvalue。lvalue可以通过std::move,表明这个内存地址可以随意操作。将rvalue传递给右值引用方法之后,他就会变成xvalue,因为只有这样我们才可以操作这个内存。
int i = 10;
std::move(i); // rvalue
void fun(std::string&& str) {
// 可以调用str.swap方法操作str
}
右值引用的问题
假如fun函数接收两个string类型的参数,如下:
void fun(const std::string& str1, const std::string& str2);
void fun(const std::string& str1, std::string&& str2);
void fun(std::string&& str1, const std::string& str2);
void fun(std::string&& str1, std::string&& str2);
这样就可以支持各种情况的右值引用了,但是如果是3个参数,4个呢?排列组合,难道要写8个方法,16个方法?C++11引入了完美转发,来解决这个问题。
完美转发
完美转发的标准写法:
std::string str;
foo(str); // fun收到string类型,lvalue
template <typename T>
void foo(T&& value) {
fun(std::forward<T>(value));
}
C++ 11标准为了更好地实现完美转发,特意为类型推到指定了新的类型匹配规则,又称为引用折叠规则(假设用 A 表示实际传递参数的类型):
- 当实参为左值或者左值引用(A&)时,函数模板中 T&& 将转变为 A&(A& && = A&)
- 当实参为右值或者右值引用(A&&)时,函数模板中 T&& 将转变为 A&&(A&& && = A&&)
这样,fun和foo收到参数类别就一致了。
解决右值引用的问题
void fun1(const std::string& str);
void fun1(std::string&& str);
void fun2(const std::string& str);
void fun2(std::string&& str);
template <typename T1, typename T2>
void foo(T1&& v1, T2&& v2) {
fun1(std::forward<T1>(v1));
fun2(std::forward<T2>(v2));
}
上面代码,有一个风险,就是如果传递给foo的是同一个string,可能就会有问题。如下测试代码:
#include <iostream>
void fun1(std::string& str) {
std::cout << "fun1 &: " << str << std::endl;
str = "fun1";
}
void fun1(std::string&& str) {
std::cout << "fun1 &&: " << str << std::endl;
std::string tmp;
tmp.swap(str);
str = "fun1 &&";
}
void fun2(std::string& str) {
std::cout << "fun2 &: " << str << std::endl;
}
void fun2(std::string&& str) {
std::cout << "fun2 &&: " << str << std::endl;
}
template <typename T1, typename T2>
void foo(T1&& v1, T2&& v2) {
fun1(std::forward<T1>(v1));
fun2(std::forward<T2>(v2));
}
int main() {
std::string str = "123";
foo(std::move(str), std::move(str));
return 0;
}
输出结果
fun1 &&: 123
fun2 &&: fun1 &&