C++11中有关右值引用的学习
文章目录
1. 左值和右值的区别
左值和右值,最早是从 C 语言继承而来的。在 C 语言,或者继承版本的解释中,
- 左值是可以位于赋值运算符 = 左侧的表达式(当然,左值也可以位于 = 的右侧),但是
- 右值是不可以位于赋值运算符 = 左侧的表达式。
对于这个经典的解释,我们有如下示例
int foo(42);
int bar(43);
// foo, bar 都是左值
foo = bar;
bar = foo;
foo = foo * bar;
// foo * bar 是右值
int baz;
baz = foo * bar; // OK: 右值在赋值运算符右侧
foo * bar = 42; // Err: 右值在赋值运算符左侧
这个解释很经典,也容易懂。不过在 C++ 里面,左值和右值不能这样定义。根据《C++ Primer》的说法,左值和右值可以这样区分:
一个表达式是左值还是右值,取决于我们使用的是它的值还是它在内存中的位置(作为对象的身份)。
也就是说一个表达式具体是左值还是右值,要根据实际在语句中的含义来确定。例如:
int foo(42);
int bar;
// 将 foo 的值赋给 bar,保存在 bar 对应的内存中
// foo 在这里作为表达式是右值;bar 在这里作为表达式是左值
// 但是 foo 作为对象,既可以充当左值又可以充当右值
bar = foo;
因为 C++ 中的对象本身可以是一个表达式,所以这里有一个重要的原则,即
- 在大多数情况下,需要右值的地方可以用左值来替代,但
- 需要左值的地方,一定不能用右值来替代。
又有一个重要的特点,即
- 左值存放在对象中,有持久的状态;而
- 右值要么是字面常量,要么是在表达式求值过程中创建的临时对象,没有持久的状态。
2. 左值引用和右值引用
在 C++ 中,有两种对对象的引用:左值引用和右值引用。
左值引用是常见的引用,所以一般在提到「对象的引用」的时候,指得就是左值引用。如果我们将一个对象的内存空间绑定到另一个变量上,那么这个变量就是左值引用。在建立引用的时候,我们是「将内存空间绑定」,因此我们使用的是一个对象在内存中的位置,这是一个左值。因此:
- 我们不能将一个右值绑定到左值引用上。
- 另一方面,由于常量左值引用保证了我们不能通过引用改变对应内存空间的值,因此我们可以将右值绑定在常量引用上。
//左值引用
int foo(42);
int& bar = foo; // OK: foo 在此是左值,将它的内存空间与 bar 绑定在一起
int& baz = 42; // Err: 42 是右值,不能将它绑定在左值引用上
const int& qux = 42; // OK: 42 是右值,但是编译器可以为它开辟一块内存空间,绑定在 qux 上,qux是常量左值引用,能够保证不修改对应内存空间的值
右值引用也是引用,但是它只能且必须绑定在右值上。
int foo(42);
int& bar = foo; // OK: 将 foo 绑定在左值引用上
int&& baz = foo; // Err: foo 可以是左值,所以不能将它绑定在右值引用上
int&& qux = 42; // OK: 将右值 42 绑定在右值引用上
int&& quux = foo * 1; // OK: foo * 1 的结果是一个右值,将它绑定在右值引用上
int& garply = foo++; // Err: 后置自增运算符返回的是右值,不能将它绑定在左值引用上
int&& waldo = foo--; // OK: 后置自减运算符返回的是右值,将它绑定在右值引用上
由于右值引用只能绑定在右值上,而右值要么是字面常量,要么是临时对象,所以:
- 右值引用的对象,是临时的,即将被销毁;并且
- 右值引用的对象,不会在其它地方使用。
敲黑板:这是重点!
这两个特性意味着:接受和使用右值引用的代码,可以*地接管所引用的对象的资源,而无需担心对其他代码逻辑造成数据破坏。
3. 引用的值类型和引用叠加
3.1 值类型
我们思考一个问题:右值引用本身是左值还是右值?或者可以先思考一下它的对偶问题:左值引用本身是左值还是右值?
先看下面的代码:
//左值引用的值类型
int foo(32);
int& bar = foo; // bar 是对foo 的左值引用
int& baz = bar; // baz 是对bar 的左值引用,因此bar 是左值
int qux = ++foo; \
// 前置自增返回的是左值引用,这里赋给qux,此时左值引用作为右值
观察上面代码,不难发现,左值引用本身既可以是左值,又可以是右值。它具体是左值还是右值,依然取决于它作为表达式时候的作用。更仔细地观察可以发现,如果左值引用作为一个变量被保存下来了,那么它就可以是左值(当然也可以起到右值的作用);而如果左值引用是一个临时变量(例如函数的返回值),那么它就是右值(如int qux = ++foo;
)
同理可以用在右值引用上:
class Type;
void foo(Type&& bar) {
// 将右值引用作为 Type 的构造函数的参数
// 此时匹配 Type::Type(const Type& orig), 即拷贝构造函数
// bar 是左值
Type baz(bar);
}
Type&& qux(); // 这里是函数
quux = qux(); // qux 的返回值是 Type 类型的右值引用,此时右值引用是右值
和左值引用一样,右值引用本身也既可以作为左值也可以作为右值。并且,同样的是:如果右值引用作为变量被保存下来了,那么应该把它当做是一个左值看待;否则应当作为右值看待。
因此,不论是左值引用还是右值引用,都有
- 当引用作为变量被保存下来,那么它是左值;否则
- 它是右值。
3.2 引用叠加
(to do…)
4. 右值引用怎么用
说了这么多右值引用的概念,应该说点实际的用途了,这样右值引用这件事情看起来才会显得自然。
4.1 move语义
假设有如下代码:
#include <iostream>
#include <string>
class Container {
private:
typedef std::string Resource;
public:
// 默认构造函数
Container() {
resource_ = new Resource;
std::cout << "default constructor." << std::endl;
}
// 拷贝构造函数,不允许隐式转换
explicit Container(const Resource& resource) {
resource_ = new Resource(resource);
std::cout << "explicit constructor." << std::endl;
}
~Container() {
delete resource_;
std::cout << "destructor" << std::endl;
}
Container(const Container& rhs) {
resource_ = new Resource(*(rhs.resource_));
std::cout << "copy constructor." << std::endl;
}
Container& operator=(const Container& rhs) {
delete resource_;
resource_ = new Resource(*(rhs.resource_));
std::cout << "copy assignment." << std::endl;
return *this;
}
private:
Resource* resource_ = nullptr;
};
Container foo() {
Container ret("tag"); //调用默认构造函数
return ret; //返回调用拷贝构造
}
int main() {
Container bar; //调用默认构造
bar = foo();
return 0;
}
运行的结果是:
default constructor.
explicit constructor.
copy assignment.
destructor
destructor
在执行 bar = foo() 的时候,会进行这样的操作:
- 从函数返回值中得到临时对象 rhs;
- 销毁 bar 中的资源(delete resource_;);
- 将 rhs 中的资源拷贝一份,赋值给 bar 中的资源(resource_ = new Resource(*(rhs.resource_)););
- 销毁 rhs 这一临时对象。
仔细想想你会发现,销毁 bar 中的资源,再从临时对象中复制相应的资源,这件事情完全没有必要。我们最好能直接抛弃 bar 中的资源而后直接接管 foo 返回的临时对象。这就是 move 语义。
这样一来,就意味着我们需要重载 Container 类的赋值操作符,它应该有这样的函数声明:Container& Container::operator=(<mystery type> rhs)
为了与拷贝版本的赋值运算符区分,我们希望,当 Container::operator= 的右操作数是右值引用时,调用这个版本的赋值运算符,那么毫无疑问, 应该是 Container&&。于是我们定义它(称为移动赋值运算符,以及同时定义移动构造函数):
#include <iostream>
#include <string>
class Container {
private:
typedef std::string Resource;
public:
Container() {
resource_ = new Resource;
std::cout << "default constructor." << std::endl;
}
explicit Container(const Resource& resource) {
resource_ = new Resource(resource);
std::cout << "explicit constructor." << std::endl;
}
~Container() {
delete resource_;
std::cout << "destructor" << std::endl;
}
Container(const Container& rhs) {
resource_ = new Resource(*(rhs.resource_));
std::cout << "copy constructor." << std::endl;
}
Container& operator=(const Container& rhs) {
delete resource_;
resource_ = new Resource(*(rhs.resource_));
std::cout << "copy assignment." << std::endl;
return *this;
}
Container(Container&& rhs) : resource_(rhs.resource_) {
rhs.resource_ = nullptr;
std::cout << "move constructor." << std::endl;
}
Container& operator=(Container&& rhs) {
Resource* tmp = resource_;
resource_ = rhs.resource_;
rhs.resource_ = tmp;
std::cout << "move assignment." << std::endl;
return *this;
}
private:
Resource* resource_ = nullptr;
};
Container foo() {
Container ret("tag");
return ret;
}
int main() {
Container bar; // default constructor.
bar = foo();
return 0;
}
// $ ./a.out
// default constructor.
// explicit constructor.
// move assignment.
// destructor
// destructor
这里程序的运行逻辑是这样的:
- 首先 程序第一行
Container bar
会调用默认构造函数 - 然后程序第二行调用
foo()
函数- 函数里面第一行调用
explicit constructor
- 函数里面第一行调用
- 回到主程序
bar = foo()
这里,调用移动赋值函数 - 然后析构函数删除
foo
的临时变量 - 最后
delete
掉bar
由此可见,这相当于我们将临时对象 rhs 中的资源「移动」到了 foo 当中,避免了销毁资源再拷贝赋值的开销。
5. 完美转发(to do)
6. std::move
标准库还定义了 std::move 函数,它的作用就是将传入的参数以右值引用的方式返回。
template<class T>
typename std::remove_reference<T>::type&&
std::move(T&& a) noexcept
{
typedef typename std::remove_reference<T>::type&& RvalRef;
return static_cast<RvalRef>(a);
}
首先,出现了两次 std::remove_reference<T>::type&&
,它确保不论 T 传入的是什么,都将返回一个真实类型的右值引用。static_cast<RvalRef>(a)
则将 a 强制转换成右值引用并返回。有了 std::move
,我们就可以调用 std::unique_ptr
的移动赋值运算符了(当然,单独这样调用可能没有什么意义):
std::unique_ptr<Type> new_ptr = std::move(old_ptr);
// old_ptr 应当立即被销毁,或者赋予别的值
// 不应对 old_ptr 当前的状态做任何假设
在这里,因为使用了 std::move
窃取了 old_ptr
中的资源,然后将他们移动到了 new_ptr
中去。这就隐含了一层意思:接下来我们不会在用 old_ptr
做任何事情,除非我们显式地对 old_ptr
赋予新值。事实上,我们不应对 old_ptr
当前的状态做任何假设,它就和已定义但未初始化的状态一样。因为,old_ptr
当前的状态,完全取决于 std::unique_ptr<Type>::operator=(unique_ptr<Type>&&) 的行为
。
7. 额外的例子
这里参考知乎的一个回答:
如何评价 C++11 的右值引用(Rvalue reference)特性? - Tinro的回答 - 知乎
https://www.zhihu.com/question/22111546/answer/30801982
8. 参考
上一篇: CSS3图片混合怎么使用