C++系列(三)右值引用


接上篇继续聊C++11的一些特性,这次说说右值引用 (这玩意一开始把我折磨的真够呛)

什么是右值?

有关C++中左值右值的定义,我们直接用C++ Primer的说法来解释:(为了最大化还原它的本意,这里放上原文)

In C++, an lvalue expression yields an object or a function. However, some lvalues, such as const objects, may not be the left-hand operand of an assignment. Moreover, some expressions yield objects but return them as rvalues, not lvalues. Roughly speaking, when we use an object as an rvalue, we use the object’s value (its contents). When we use an object as an lvalue, we use the object’s identity (its location in memory).

看下面的例子:

// lvalues:
int i = 42;
i = 43; // ok, i is an lvalue
int* p = &i; // ok, i is an lvalue
int& foo();
foo() = 42; // ok, foo() is an lvalue
int* p1 = &foo(); // ok, foo() is an lvalue

// rvalues:
int foobar();
int j = 0;
j = foobar(); // ok, foobar() is an rvalue
int* p2 = &foobar(); // error, cannot take the address of an rvalue
j = 42; // ok, 42 is an rvalue

通常来说,左值是指一个具有持久的内存地址的对象,而右值是指临时对象或字面量值。C++中的左值和右值也具有着不同的用途和行为。例如,左值可以取地址并用作指针,而右值不能取地址。此外,左值和右值在传递参数和返回值时也具有不同的语义和行为,这些在后面会详细说明。

所以根据这些定义就引申出很重要的一句话:在需要左值的地方,一定不能用右值替代,但是在大多数情况下(并不是全部!),用右值的地方也可以用左值替代。当左值出现在需要右值的地方时,它被使用的是它的值,而不是它的内存位置。

什么是右值引用?

既然值的类型分为了左值与右值,那么相对应的,我们就有了左值引用和右值引用。左值引用就是我们最常见的引用,而右值引用则很直观的,就是绑定在右值上的引用。在C++11中,区别于左值引用,右值引用使用&&。

int a = 10;  // a is a lvalue
int &la = a;  // ok: la is a lvalue reference
int &&ra = a;  // err: cannot bind rvalue to lvalue

int &lla = a + 1;  // err: cannot bind lvalue to rvalue
int &&rra = a + 1;  // ok: bind rra to result of a + 1

左值与右值的区别除了上一部分提到的之外,还有一点最大的区分就是,左值拥有持久化的状态,而右值则是literals或者临时对象。而且正因为右值只有可能被绑定到临时的对象上,我们有了下面这两条rule:

  • 右值绑定的对象是即将被销毁的
  • 右值绑定的对象不会有其他的使用者

所以综合这些特点来看,右值引用的使用者可以安全的接手这个引用绑定的对象的资源,而无需担心其他。

哪些地方用到右值引用?

std::move()

move函数告诉编译器,我们想将一个左值cast成其相对应的右值引用(当然如果不用它,我们也可以显式的手动cast)。

vector<string> v;
string str = "abc";
v.push_back(str);  // str is still "abc"
v.push_back(std::move(str));  // str now becomes ""

下面来看一下move的实现。

template<typename T>
typename remove_reference<T>::type&& move(T&& t) {
    return static_cast<typename remove_reference<T>::type&&>(t);
}

在我们去理解这一段代码之前,需要先说一说引用折叠。

虽然名词看着很高大上,但是简单来说它的意思就是在特定情况下,左值引用和右值引用会被折叠成一个引用类型。这些特定情况如下:

  1. 当一个对象被声明为右值引用类型时,如果再将一个右值引用绑定到该对象上,这两个右值引用会被折叠成一个右值引用。

  2. 当一个对象被声明为左值引用类型时,如果再将一个左值引用绑定到该对象上,这两个左值引用会被折叠成一个左值引用。

    obj &  &  = obj &
    obj &  && = obj &
    obj && &  = obj &
    obj && && = obj &&
    

这一切的一切,都是为了后面我们要说到的完美转发做准备。明白了这些,我们再来看具体的执行过程。当我们调用一个std::move()并传入一个左值时,

obj a;
obj b = std::move(a);

上文提到的move会被实例化成,

remove_reference<obj&>::type&& move(obj& && arg)
{
  return static_cast<remove_reference<obj&>::type&&>(arg);
}

我们知道了引用折叠,所以这里的obj& &&就变成了obj &。remove_reference在这里的作用顾名思义,就是给传入的type去除reference,否则在引用折叠的时候返回值也会被折叠成一个左值引用,这样move就失去了它的意义。最终我们得到的函数如下:

obj&& move(obj & arg)
{
  return static_cast<obj&&>(arg);
}

现在是不是看起来好理解多了?它不过是把一个传入的左值cast成一个右值然后返回。

同样的,假如我们传入一个右值到move的话,例如

obj a = std::move(obj());

这里可以看到obj()是一个临时对象,符合右值的定义。那么这个时候,move会被实例化成

remove_reference<obj>::type&& move(obj&& arg)
{
  return static_cast<remove_reference<obj>::type&&>(arg);
}

根据引用折叠以及之前所说,move最终会被简化成

obj&& move(obj&& arg)
{
  return static_cast<obj&&>(arg);
}

所以从不严格意义上来说,它传入的是一个右值,返回的依然是这个右值,什么也没干。至于为什么还需要这么一个static_cast, 是因为被命名的右值引用会被当作一个左值,而这两者之间的隐式转换是不被标准允许的。

移动构造函数 / 移动赋值运算符

移动构造函数类似于拷贝构造函数,都是将另一个对象的资源转移到当前的对象。不同的则是移动构造函数不需要复制一份,而是直接将另一个对象的资源拿过来。

// copy constructor
obj (const obj& other) {
  resource = new char[100];
  memcpy(resource, other.resource, 100);
}
// move constructor
obj (const obj&& other) {
  resource = other.resource;
  other.resource = nullptr;  // IMPORTANT!
}

移动赋值运算符则类似于拷贝复制运算符,类似的,它也是略过了复制的步骤,直接将另一对象的资源拿过来。

// copy assignment
obj& operator=(const obj& other) {
  if(this != &other) {
    delete resource;
    resource = new char[100];
    memcpy(resource, other.resource, 100);
  }
  return *this;
}
// move assignment
obj& operator=(const obj&& other) {
  if(this != &other) {
    delete resource;
    resource = other.resource;
    other.resource = nullptr; 
  }
  return *this;
}

具体的使用方式还是要结合std::move()来展示:

obj a;
obj b = std::move(a);  // a becomes empty
obj c(std::move(b));  // b becomes empty
// now only c holds the resource

这一篇关于右值引用的东西就到这里吧,有关完美转发我打算留到下一篇再讲,因为可以说的东西太多了,同时也会说说万能引用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

dabtwice

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

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

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

打赏作者

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

抵扣说明:

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

余额充值