一直想试着把自己理解和学习到的右值引用相关的技术细节整理并分享出来,希望能够对感兴趣的朋友提供帮助。

右值引用是C++11标准中新增的一个特性。右值引用允许程序员可以忽略逻辑上不需要的拷贝;而且还可以用来支持实现完美转发的函数。它们都是实现更高效、更健壮的库。

move语义

先不展开具体右值引用定义。先说说move语义。右值引用是用来支持move语义的。move语义是指将一个同类型的对象A中的资源(可能是在堆上分配,也可能是一个文件句柄或者其他系统资源)搬移到另一个同类型的对象B中,解除对象A对该资源的所有权。这样可以减少不必要的临时对象的构造、拷贝以及析构等动作。比如我们经常使用的std::vector<T>,当两个相同的std::vector类型赋值时,一般的步骤如下:

  1. 内部的赋值构造函数一般是先分配指定大小的内存,
  2. 从源std::vector中拷贝到新申请的内存,
  3. 之后再把原有的对象实例析构掉,
  4. 最后接管新申请的数据。

这就是我们C++11之前使用的拷贝语义,也就是常说的深拷贝。move语义与拷贝语义相对,类似于浅拷贝,但是资源的所有权发生了转移。move语义的实现可以减少拷贝动作,大幅提高程序的性能。

而为了实现move语义的构造,就需要对应的语法来支持。原有的拷贝构造函数等不能够满足该需求。最典型的例子就是C++11废弃的std::auto_ptr,其构造函数会产生不明确的拥有权关系,很容易滋生BUG。这也是很多人不喜欢std::auto_ptr的原因。C++11为此增加了相应的构造函数。

class Foo {
public:
Foo(Foo&& f) {}
Foo& operator=(Foo&& f) {
return *this;
}
};

这里可以明显看到两个函数中的参数类型是Foo&&。这就是右值引用的基本语法。这样做的目的是通过函数重载实现不同的功能处理。

强制move语义

C++11规定即可以在右值上使用move语义,也可以在左值上使用move语义。也就是说,可以把一个左值转为右值引用,然后使用move语义。比如在C++的经典函数swap中:

template<class T>
void swap(T& a, T& b)
{
T tmp(a);
a = b;
b = tmp;
} X a, b;
swap(a, b);

上面代码中没有右值,但是tmp变量只作用在本函数作用域中,只是用来承担数据的转移动作。C++11制定的上述规则在这里反而可以得到非常好的适用。C++11为了达到这个规则,实现了std::move函数,这个函数的就是把传入的参数转换为一个右值引用并返回。也就是说在C++11下,swap的实现如下:

template<class T>
void swap(T& a, T& b)
{
T tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
} X a, b;
swap(a, b);

我们在实际使用中,也可以尽量的多使用std::move。只要求我们自定义的类型实现转移构造函数。

右值引用

为了说清楚右值引用什么,就不得不说左值和右值。简单的说左值是一个指向某内存空间的表达式,并且我们可以用&操作符获得该内存空间的地址。右值就是非左值的表达式。可以阅读这篇《Lvalues and Rvalues》进行深入理解。

右值引用非常类似于C++的普通引用,也是一个复合类型。为了方便区分,普通引用就是左值引用。一个左值引用就是在类型后面加&操作符。而右值引用就是在类型后加&&操作符,就像上面的转移构造函数的参数一样。

右值引用的行为类似于左值引用,但是右值引用只能绑定临时对象,不能绑定一个左值引用。右值引用的出现还影响了函数重载决议。左值会优先适配左值引用参数的函数,右值会优先适配右值引用参数的函数:

void foo(X& x); // lvalue reference overload
void foo(X&& x); // rvalue reference overload X x;
X foobar(); foo(x); // argument is lvalue: calls foo(X&)
foo(foobar()); // argument is rvalue: calls foo(X&&)

理论上,你可以用这种方式重载任何函数,但是绝大多数情况下这样的重载只出现在拷贝构造函数和赋值运算符中,也就是实现move语义。

如果你实现了void foo(X&);,但是没有实现void foo(X&&);,那么和以前一样foo的参数只能是左值。如果实现了void foo(X const &);,但是没有实现void foo(X&&);,仍和以前一样,foo的参数既可以是左值也可以是右值。唯一能够区分左值和右值的办法就是实现void foo(X&&);。最后,如果只实现了实现void foo(X&&);,但却没有实现void foo(X&);和void foo(X const &);,那么foo的参数将只能是右值。

右值引用是右值吗?

void foo(X&& x)
{
X anotherX = x;
// ...
}

在上面这个函数foo内,X的哪个构造函数会被调用?是拷贝构造还是转移构造?按照我们之前说的,这是个右值引用,应该是调用的X(X&&);函数。但是实际上,这里调用的是X(const X&);这里就是让人迷惑的地方:右值引用类型既可以被当做左值也可以被当做右值,判断的标准是该右值引用是否有名字。有名字就是左值,否则就是右值。如果要做到把带有名字的右值引用变为右值,就需要借助std::move函数。

void foo(X&& x)
{
X anotherX = std::move(x);
// ...
}

在实现自己的转移构造函数时,一些人没有理解这一点,导致在自己的转移构造函数内部的实现中实际是执行了拷贝构造函数。

move语义与返回值优化

了解了move语义和强制move以及右值引用的一些概念后,有些朋友在实现一些函数时,会在返回的地方进行强制move。认为这样可以减少一次拷贝。比如:

X foo()
{
X x;
// perhaps do something to x
return std::move(x); // making it worse!
}

实际上这种是不需要的。因为编译器会做返回值优化(Return Value Optimization)。在C++11标准中有如下规定:

When the criteria for elision of a copy operation are met or would be met save for the fact that the source object is a function parameter, and the object to be copied is designated by an lvalue, overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue.

直接return x;是NRVO支持的一种用例场景,可以做到多余的拷贝构造。编译器会自己选择使用拷贝构造还是move构造函数。

但是如果用std::move(x);那么可能会带来额外的影响:可能会阻止NRVO。也就是说可能需要额外的开销来执行move语义。

建议阅读Stackoverflow上的这两个问题:

《When should std::move be used on a function return value?》

《Why does std::move prevent RVO?》

完美转发

右值引用除了用来实现move语义之外,还就是为了解决完美转发的问题。我们有的时候会写工厂函数,比如如下代码:

template<typename T, typename Arg>
shared_ptr<T> factory(Arg arg)
{
return shared_ptr<T>(new T(arg));
}

这个实现非常简单,就是把参数arg传给类T进行构造。但是这里引入了额外的通过值的函数调用,不使用于那些以引用为参数的构造函数。

那么为了解决这个问题,就有人想到用引用,比如:

template<typename T, typename Arg>
shared_ptr<T> factory(Arg& arg)
{
return shared_ptr<T>(new T(arg));
}

但是这里又有问题,不能接收右值作为参数。

factory<X>(hoo()); // error if hoo returns by value
factory<X>(); // error

对应的解决办法是继续引入const引用。如果有多个参数的情况下,这个函数的参数列表就变的比较恶心了。同时还有个问题就是不能实现move语义。

而右值引用可以解决这个问题,可以不用通过重载函数来实现真正的完美转发。但是它需要配合两个右值引用的规则:

  • 引用叠加规则
A& & => A&
A& && => A&
A&& & => A&
A&& && => A&&
  • 模板参数推导规则
template<typename T>
void foo(T&&);

当函数foo的实参是一个A类型的左值时,T的类型是A&。再根据引用叠加规则判断,最后参数的实际类型是A&。
当foo的实参是一个A类型的右值时,T的类型是A。根据引用叠加规则可以判断,最后的类型是A&&。

有了上面这些规则,我们可以用右值引用来解决前面的完美转发问题。下面是解决的办法:

template<typename T, typename Arg>
shared_ptr<T> factory(Arg&& arg)
{
return shared_ptr<T>(new T(std::forward<Arg>(arg)));
}

而std::forward的实现如下:

template<class S>
S&& forward(typename remove_reference<S>::type& a) noexcept
{
return static_cast<S&&>(a);
}

这里就不展开具体的例子来说明了,明白了上述的两个规则就可以明白了。建议阅读Scott Meyers的《Universal References in C++11》.

参考文档

  1. 《C++ Rvalue References Explain》
  2. 《Universal References in C++11》
  3. 《A Brief Introduction to Rvalue References》

总结

右值引用的出现虽然看似增加了额外的复杂度,但是它带来的收益还是非常明显的,能够帮助实现move语义,提升程序的性能;又可以实现完美转发,方便了库的设计。

C++就是这样,给你一个新增一个特性之后,也会带来额外的学习难度。但是这也是很多人喜欢C++的原因,它给了程序员太多的可能性。可以精准的控制对象的生命周期,是高性能程序必不可少的工具。

C++右值引用浅析的更多相关文章

  1. C++ 11 中的右值引用

    C++ 11 中的右值引用 右值引用的功能 首先,我并不介绍什么是右值引用,而是以一个例子里来介绍一下右值引用的功能: #include <iostream>    #include &l ...

  2. 图说函数模板右值引用参数(T&&)类型推导规则(C++11)

    见下图: 规律总结: 只要我们传递一个基本类型是A④的左值,那么,传递后,T的类型就是A&,形参在函数体中的类型就是A&. 只要我们传递一个基本类型是A的右值,那么,传递后,T的类型就 ...

  3. c++11的右值引用、移动语义

    对于c++11来说移动语义是一个重要的概念,一直以来我对这个概念都似懂非懂.最近翻翻资料感觉突然开窍,因此记下.其实搞懂之后就会发现这个概念很简单,并无什么高深的地方. 先说说右值引用.右值一般指的是 ...

  4. VS2012 error C2664: “std::make_pair”:无法将左值绑定到右值引用

    在vs2012(c++)make_pair()改动: C++: template <class T1, class T2> pair<V1, V2> make_pair(T1& ...

  5. 右值引用、move与move constructor

    http://blog.chinaunix.net/uid-20726254-id-3486721.htm 这个绝对是新增的top特性,篇幅非常多.看着就有点费劲,总结更费劲. 原来的标准当中,参数与 ...

  6. 【转】C++11 标准新特性: 右值引用与转移语义

    VS2013出来了,对于C++来说,最大的改变莫过于对于C++11新特性的支持,在网上搜了一下C++11的介绍,发现这篇文章非常不错,分享给大家同时自己作为存档. 原文地址:http://www.ib ...

  7. move语义和右值引用

    C++11支持move语义,用以避免非必要拷贝和临时对象. 具体内容见收藏中的“C++右值引用” .

  8. [转载] C++11中的右值引用

    C++11中的右值引用 May 18, 2015 移动构造函数 C++98中的左值和右值 C++11右值引用和移动语义 强制移动语义std::move() 右值引用和右值的关系 完美转发 引用折叠推导 ...

  9. C++ 11 右值引用

    C++11中引入的一个非常重要的概念就是右值引用.理解右值引用是学习“移动语义”(move semantics)的基础.而要理解右值引用,就必须先区分左值与右值. 注意:左值右值翻译可能有些问题 *L ...

随机推荐

  1. Unity3d学习 相机的跟随

    最近在写关于相机跟随的逻辑,其实最早接触相机跟随是在Unity官网的一个叫Roll-a-ball tutorial上,其中简单的涉及了关于相机如何跟随物体的移动而移动,如下代码: using Unit ...

  2. jQuery实践-网页版2048小游戏

    ▓▓▓▓▓▓ 大致介绍 看了一个实现网页版2048小游戏的视频,觉得能做出自己以前喜欢玩的小游戏很有意思便自己动手试了试,真正的验证了这句话-不要以为你以为的就是你以为的,看视频时觉得看懂了,会写了, ...

  3. nginx配置反向代理或跳转出现400问题处理记录

    午休完上班后,同事说测试站点访问接口出现400 Bad Request  Request Header Or Cookie Too Large提示,心想还好是测试服务器出现问题,影响不大,不过也赶紧上 ...

  4. .Net中的AOP系列之构建一个汽车租赁应用

    返回<.Net中的AOP>系列学习总目录 本篇目录 开始一个新项目 没有AOP的生活 变更的代价 使用AOP重构 本系列的源码本人已托管于Coding上:点击查看. 本系列的实验环境:VS ...

  5. 我们是怎么做Code Review的

    前几天看了<Code Review 程序员的寄望与哀伤>,想到我们团队开展Code Review也有2年了,结果还算比较满意,有些经验应该可以和大家一起分享.探讨.我们为什么要推行Code ...

  6. Win10 IIS本地部署MVC网站时不能运行?

    异常处理汇总-服 务 器 http://www.cnblogs.com/dunitian/p/4522983.html 部署后出现这个错误: 打开文件目录后发现是可以看见目录的,静态页面也是可以打开的 ...

  7. gulp初学

    原文地址:gulp初学 至于gulp与grunt的区别,用过的人都略知一二,总的来说就是2点: 1.gulp的gulpfile.js  配置简单而且更容易阅读和维护.之所以如此,是因为它们的工作方式不 ...

  8. C#与C++通信

    # C#与C++相互发送消息 # ## C#端: ## namespace CshapMessage { /// /// MainWindow.xaml 的交互逻辑 /// public partia ...

  9. redis成长之路——(二)

    redis操作封装 针对这些常用结构,StackExchange.Redis已经做了一些封装,不过在实际应用场景中还必须添加一些功能,例如重试等 所以对一些常功能做了一些自行封装SERedisOper ...

  10. BlockingCollection使用

    BlockingCollection是一个线程安全的生产者-消费者集合. 代码 public class BlockingTest { BlockingCollection<int> bc ...