标题: C++右值、移动、完美转发
作者: 边城量子(shihezichen@live.cn)
本文通过例子讲解C++中的左值/右值概念、 移动概念、完美转发概念, 帮助理解移动构造函数、通用引用、完美转发。
移动构造函数
1. 在没有移动构造函数之前哪里有性能瓶颈?
int main(int argc, char *argv[]){
std::vector<A> vec;
vec.push_back(A());
}
在main函数中,定义了一个A类型的vector,然后用A类型创建一个对象,然后把它放入vector. 这段代码有较大的性能问题,因为在A对象放入到vector时,在vector内部又创建了一个对象,并调用其拷贝构造函数进行了深拷贝。如果在A对象中分配的是一个比较大的空间,且vector中要存放大量的A对象时(如 100000个),就会不断的做分配/释放堆空间的操作,这会造成很大的性能消耗。
2. 添加移动构造函数
class A {
public:
...
A(A&& a){
std::cout << "A move construct ..." << std::endl;
ptr_ = a.ptr_;
a.ptr_ = nullptr;
}
...
};
int main(int argc, char *argv[]){
std::vector<A> vec;
vec.push_back(std::move(A()));
}
移动构造函数参数是 A&& a,代表它需要一个右值参数,但是A()是一个左值,因此需要通过std::move(A())
转成右值。vector内部通过移动构造函数创建A对象,减少了对堆空间的频繁操作。
左值和右值
概念
保存在CPU寄存器中的值为右值,而保存在内存中的值为左值。 左值(lvalue): locator value, 可存储在内存中,有明确存储地址(可寻址)的数据 右值(rvalue): read value, 可以提供数据值的数据(不一定可寻址, 比如存在寄存器中的数据)
程序运行时并不是直接从内存中取令运行的,因为内存相对于CPU来说太慢了。一般情况下都是先将一部分指令读到CPU的指令寄存器,CPU再从指令寄存器中取指令然后一条一条的执行。对于数据也是一样,先将数据从内存中读到数据寄存器,然后CPU从数据寄存器读数据。
一个常数5,是放在寄存器中,在C++中是一个右值。定义一个变量a,它在内存中会分配空间,在C++中是一个左值。a+5的结果是右值,放到寄存器中。
例子
int&& a = 5; // 正确, 5直接存放在寄存器中,它是右值;
// a是左值,但是特殊点在于它只能接受右值赋值
int b = 10;
int&& c = b; // 错误,b在内存中,是右值,不能赋值给左值引用
int&& d = b + 5; // 正确,虽然b在内存中,但b+5的结果存放在寄存器中,是右值
int&& e = a; // 错误,a虽然接受了右值,但它自己是左值,无法复制给e,e只能接受右值
int& f = 10; // 错误, 10是右值, &表示左值引用, 无法为右值建立左值引用
int&& g = 10; // 正确
通用引用
通用引用既可以接受左值,也可以接受右值。
例子:
int a = 123;
auto&& b = 5; // 通用引用,可以接收右值
int&& c = a; // 错误
auto&& d = a; // 通用引用,可以接收左值
const auto&& e = a; //错误, 加了const就不再是通用引用
template<typename T>
void f(T&& param){
}
完美转发
完美转发指的是函数模板可以将自己的参数"完美"地转发给内部调用的其他函数,所谓完美,即不仅能准确的转发参数的值,还能保证被转发参数的左、右值属性不变。
未实现完美转发的例子:
template <typename T>
void function(T t)
{
orderdef(t);
}
对上述例子, 完美转发指的是: 如果function
函数接收到的参数t
无论为左值还是右值, 都原封不动的传递给otherdef()
, 很显然,上述例子未实现完美转发, 参数t
为非引用类型,因此调用function
函数时,实参将值传递给形参的过程就要额外进行一次拷贝操作,然后保存在一个内部的参数t中,它永远为左值,因此orderdef
的参数也永远为左值.
修改为如下形式:
template <typename T>
void function(T&& t)
{
otherdef(std::forward<T>(t));
}
- 此模板函数的参数t既可以接受左值,也可以接受右值.
function
内部形参原本既有名称又能寻址, 它是左值, 通过模板函数forward<T>()
修饰后就能保持参数的左/右值属性.
完美转发的原理:
template <typename T>
T&& forward(typename std::remove_reference<T>::type& param)
{
return static_cast<T&&>(param);
}
template <typename T>
T&& forward(typename std::remove_reference<T>::type&& param)
{
return static_cast<T&&>(param);
}