C++右值引用浅析

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

右值引用是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)。也就是说编译器能够感知到x变量不再需要了,它需要转移到函数外部使用。

完美转发

右值引用除了用来实现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>(41); // 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++的原因,它给了程序员太多的可能性。可以精准的控制对象的生命周期,是高性能程序必不可少的工具。

时间: 2024-11-03 22:24:17

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

C11右值引用

 int *a = &1; //1为右值 不可取址  const int &&aa = 1;// 右值引用,    //注意  考虑到安全因素,具名变量即使被声明为右值类型也不会被当作右值 而要用std::move函数

C++11之右值引用(二):右值引用与移动语义

上节我们提出了右值引用,可以用来区分右值,那么这有什么用处?   问题来源   我们先看一个C++中被人诟病已久的问题: 我把某文件的内容读取到vector中,用函数如何封装? 大部分人的做法是: void readFile(const string &filename, vector<string> &words) { words.clear(); //read XXXXX } 这种做法完全可行,但是代码风格谈不上美观,我们尝试着这样编写: vector<string&

【转载】C++ 11中的右值引用

本篇随笔为转载,原博地址如下:http://www.cnblogs.com/TianFang/archive/2013/01/26/2878356.html 右值引用的功能 首先,我并不介绍什么是右值引用,而是以一个例子里来介绍一下右值引用的功能: #include <iostream>    #include <vector>    using namespace std; class obj    {    public :        obj() { cout <&l

Item 24: 区分右值引用和universal引用

本文翻译自<effective modern C++>,由于水平有限,故无法保证翻译完全正确,欢迎指出错误.谢谢! 古人曾说事情的真相会让你觉得很自在,但是在适当的情况下,一个良好的谎言同样能解放你.这个Item就是这样一个谎言.但是,因为我们在和软件打交道,所以让我们避开"谎言"这个词,换句话来说:本Item是由"抽象"组成的. 为了声明一个指向T类型的右值引用,你会写T&&.因此我们可以"合理"地假设:如果你在源代

c++11之右值引用和std::move

这两个特性是c++11里比较有性能提升意义的.个人认为这两个特性也体现了c++对性能提升的极限追求. 通过改写经典c++面试题mystring来体会 move不能减少临时变量的产生,但是可以减少内存的维护量 代码 //右值引用 /* 左值对象:持久存在的对象,具有名字,可以对其去地址 右值对象:临时对象,表达式结束后它就没了,不能对它取地址,它也没有名字~ 右值引用类型:引用右值的类型,用&&来表示 */ /*****************************************

翻译「C++ Rvalue References Explained」C++右值引用详解 Part3:右值引用

本文为第三部分,目录请参阅概述部分:http://www.cnblogs.com/harrywong/p/4220233.html. 右值引用 如果x是任意类型,那么x&&则被称作一个对x的右值引用(rvalue reference).为了更好区分,原来的引用x&现在也被称作左值引用(lvalue reference). 一个右值引用是一种同原始引用x&的行为非常类似的类型,但是有一些特例.最重要的一个就是当面临方法重载决议的时候,左值倾向于旧式的左值引用,而右值偏向于新的

第二十四章 C++11特性之右值引用

右值引用,是 C++11 语言核心中最为重要的改进之一.右值引用给 C++ 带来了“Move语义”(“转移语义”),同时解决了模板编程中完美转发的问题(Perfect forwarding).右值引用使 C++ 对象有能力甄别什么是(可以看作)临时对象,对于临时对象的拷贝可以做某种特别的处理,一般来说主要是直接传递资源的所有权而不是像一般地进行拷贝,这就是所谓的 move 语义了.完美转发则是指在模板编程的时候,各层级函数参数传递时不会丢失参数的“属性”(lvalue/rvalue, const

C++11标准之右值引用(rvalue reference)

1.右值引用引入的背景 临时对象的产生和拷贝所带来的效率折损,一直是C++所为人诟病的问题.但是C++标准允许编译器对于临时对象的产生具有完全的自由度,从而发展出了Copy Elision.RVO(包括NRVO)等编译器优化技术,它们可以防止某些情况下临时对象产生和拷贝.下面简单地介绍一下Copy Elision.RVO,对此不感兴趣的可以直接跳过: (1) Copy Elision Copy Elision技术是为了防止某些不必要的临时对象产生和拷贝,例如: struct A { A(int)

详解C++右值引用

http://jxq.me/2012/06/06/%E8%AF%91%E8%AF%A6%E8%A7%A3c%E5%8F%B3%E5%80%BC%E5%BC%95%E7%94%A8/#thbecker C++0x标准出来很长时间了,引入了很多牛逼的特性[1].其中一个便是右值引用,Thomas Becker的文章[2]很全面的介绍了这个特性,读后有如醍醐灌顶,翻译在此以便深入理解. 目录 概述 move语义 右值引用 强制move语义 右值引用是右值吗? move语义与编译器优化 完美转发:问题