前言
前段时间换工作,技术栈渐渐从Android偏向到跨端开发,需要重拾C++,所以打算总结成一个系列。
这个系列会从实用的角度总结C++11的一些新特性,尽量做到简单通俗易懂。
面对人群是有C++基础想快速了解现代C++新特性的同学,感觉比较多,大学基本都是教传统C++。
废话不多说,本文讲一下右值引用的概念和以及它的应用场景移动语义。
右值引用的由来
在C++11之前,所有引用都是左值引用(lvalue reference),也就是对左值的引用。左值一般放在赋值表达式左边,是在堆或栈上分配的命名对象,它们有明确的内存地址。
而左值的另一位朋友右值(rvalue),在赋值表达式右边,没有可识别的内存地址。如果从硬件层面理解,右值只存在于临时寄存器中。
比如下面这段代码:
int a = 1;
int& b = a;
很明显,这里 a 是左值,1 是右值,b 是一个左值引用,也就是a的别名。
再比如这段:
int& a = 1;
g++编译,会显示错误如下:
non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'
意思是非常量左值引用不能指向右值。
大家都不会犯这样的错,这里想说的是,我们还可以使用常量左值引用来指向右值,像这样:
const int& a = 1; //常量左值引用
问题来了,常量左值引用为什么可以指向右值?
因为const常量值不可修改,可以理解为内部产生了一个临时量,可以取到地址。类似于以下:
const int tmp = 1;
const int &a = tmp;
可以看到,const Type& 是 C++ 中一个常见的习惯,函数的参数使用常量引用 const Type& 接收,以避免创建不必要的临时对象:
void func(const std::string& a);
func("hello");
但是这种方式有个缺点,就是没法修改这个const常量,有一定局限性。
C++11引入的这位新朋友,右值引用,一定程度上解决了其中的这个问题。
右值引用,Type&&,用来指向右值,并且可以修改右值。
void func(const std::string&& a){
a = "world"; //修改右值
}
func("hello");
OK,到这里,我们简单总结下
- 左值可以寻址,右值不可以寻址,这是它们的关键区别;
- 函数传参使用左右值引用可以避免拷贝,但右值引用更为灵活。
那么,右值引用的具体应用场景是什么?
移动语义提升性能
右值引用有一个非常重要的作用是支持移动语义。而相对于移动语义,拷贝语义可能比较好理解。
比如下面代码,我们可以定义拷贝构造函数来实现对象的深拷贝,如果没有定义,编译器会有默认实现,是浅拷贝。
class Stack
{
public:
Stack(int size = 100) : size_(size)
{
cout << "构造函数" << endl;
stack_ = new int[size];
}
Stack(const Stack &src):size_(src.size_)
{
cout << "拷贝构造函数" << endl;
stack_ = new int[src.size_];
//深拷贝
for (int i = 0; i < size_; ++i)
{
stack_[i] = src.stack_[i];
}
}
~Stack()
{
cout << "析构函数" << endl;
delete[] stack_;
stack_ = nullptr;
}
private:
int size_;
int *stack_;
};
int main()
{
Stack stack(10);
Stack stack2 = stack;
}
运行输出是:
构造函数
拷贝构造函数
析构函数
析构函数
除此之外,在某些场景,比如被拷贝者之后不再需要,我们其实可以使用 std::move 触发移动语义,避免深拷贝,提升性能。
所以在上面代码中,我们可以加一个移动构造函数,这种方式在STL和自定义类广泛应用。
Stack(Stack&& src):size_(src.size_) {
cout << "移动构造函数" << endl;
stack_ = src.stack_;
src.stack_ = nullptr;
}
int main(){
Stack stack(10);
//Stack stack2 = stack; //走拷贝构造
Stack stack2 = std::move(stack); //走移动构造
}
运行的输出是:
构造函数
移动构造函数
析构函数
析构函数
这里,std::move 的作用是把左值转换为右值引用,而移动构造函数的作用是传入对象的所有权转让给当前对象,然后掏空了传入对象。
std::move真面目
大家可能以为std::move施展了什么神奇的魔法,其实并没有,仅仅做了类型转换而已,真正的移动操作是在移动构造函数或者移动赋值操作符中发生的。可以瞧一瞧代码
template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
发现只是一个static_cast转换,其中,remove_reference的作用去除T中的引用部分,无论T是左值还是右值,只获取其中的类型。我们来简化一下,当_TP是string时,这个函数其实就是
string&& move(string&& __t)
{
return static_cast<string&&>(__t);
}
所以,不管传参是左值右值,最后返回的一定是个右值引用。
实际上,std::move 运行期不做任何事情,因为编译后不会生成可执行代码,内部只是变量地址的透传,完全可以被优化掉。有兴趣的同学可以看看
通过汇编浅析 C++ 右值引用
最后,聪明的你可能也发现了,std::move 的函数参数&&看起来是一个右值引用类型,可以传入左值吗?