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的临时变量
  • 最后deletebar

由此可见,这相当于我们将临时对象 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. 参考

  1. 谈谈 C++ 中的右值引用
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zedjay_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值