C++ 右值引用

block://6984617523950616580?from=docs_block&id=ce31003ceb5efb1f7a7c0a5fbe6cb60191627a38

右值的引入

作为在C++11中引入的一个类型,容易引起误解的是,右值引用并没有说明引入是为了什么,是为了解决什么问题。

右值引用可以解决以下问题

  1. 实现移动语义
  2. 完美转发

左值和右值来自原先的C语言,左值可以出现在赋值左边或者右边,而右值只能出现在赋值的右边

int a = 42;
int b = 43;// a and b are both l-values:
a = b; // ok
b = a; // ok
a = a * b; // ok// a * b is an rvalue:
int c = a * b; // ok, rvalue on right hand side of assignment
a * b = 42; // error, rvalue on left hand side of assignment

在 C++ 中,这作为第一个直观的左值和右值方法仍然很有用。但是,带有用户定义类型的 C++ 引入了一些关于可修改性和可分配性的微妙之处,导致此定义不正确。我们没有必要进一步讨论这个问题。这是一个替代定义,尽管它仍然存在争议,但它将使您能够处理右值引用:左值是一个引用内存位置的表达式,并允许我们通过&操作符取得地址,右值,不是左值的都是右值。

// 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

移动语义

假设有一个类X,类中的成员变量m_pResource是一个需要花费时间和内存取进行构造和析构的类型,比如m_pResource是一个vector类型,对其进行赋值时将会产生大量的析构和构造函数的调用。

X& X::operator=(X const & rhs)
{// [...]// Make a clone of what rhs.m_pResource refers to.// Destruct the resource that m_pResource refers to. // Attach the clone to m_pResource.// [...]
}

同样的问题会出现在copy构造函数上

X foo();
X x;
// perhaps use x in various ways
x = foo();
  • clones the resource from the temporary returned by foo,

  • destructs the resource held by x and replaces it with the clone,

  • destructs the temporary and thereby releases its resource.

当赋值操作符的右边是右值的话,只是交换值的指针是比较高效的

// [...]
// swap m_pResource and rhs.m_pResource
// [...]

上述这种操作就是移动语义,可以通过操作符重载实现

X& X::operator=(<mystery type> rhs)
{// [...]// swap this->m_pResource and rhs.m_pResource// [...]
}

以上调用无论是赋值还是copy构造函数,都会导致大量的构造函数和析构函数调用(如当vector中存储很多的类对象时),因此我们当然希望能够实现对传入类型的引用,从而避免这些构造函数和析构函数的调用

block://6984620384730546178?from=docs_block&id=ce31003ceb5efb1f7a7c0a5fbe6cb60191627a38

右值引用

如果X是一个类型,那么X&& 就是对X类型的右值引用,为了更好的区分X&被称为左值引用

一个右值引用类型很多地方表现与左值引用相同,除了一些例外。最重要的一条就是,当进行函数重载的时候,左值当成参数传入函数,偏向调用左值引用的函数;当右值传入函数时,更加偏向调用右值重载的函数

void foo(X& x); // 左值函数重载
void foo(X&& x); // 右值函数重载X x;
X foobar();foo(x); // argument is lvalue: calls foo(X&)
foo(foobar()); // argument is rvalue: calls foo(X&&)

Rvalue references allow a function to branch at compile time (via overload resolution) on the condition “Am I being called on an lvalue or an rvalue?”

大体意思就是,右值引用允许编译器期间通过是右值还是左值调用不同的函数

当然你可以使用上述方法重载任何函数,就像上述所示。但是通常会被用于重载拷贝构造函数和赋值构造函数,用来实现移动语义

X& X::operator=(X const & rhs); // classical implementationX& X::operator=(X&& rhs)
{// Move semantics: exchange content between this and rhsreturn *this;
}
Note: If you implement
void foo(X&);
but not
void foo(X&&);
then of course the behavior is unchanged: foo can be called on l-values, but not on r-values. If you implement
void foo(X const &);
but not
void foo(X&&);
then again, the behavior is unchanged: foo can be called on l-values and r-values, but it is not possible to make it distinguish between l-values and r-values. That is possible only by implementing
void foo(X&&);
as well. Finally, if you implement
void foo(X&&);
but neither one of
void foo(X&);
and
void foo(X const &);
then, according to the final version of C++11, foo can be called on r-values, but trying to call it on an l-value will trigger a compile error.

强制移动语义

我们都知道,在给予更多控制权和避免粗心大意犯错方面C++选择给予更多的控制权,你不但可以在右值上实现移动语义,而且你可以自行决定在左值上实现移动语义,一个很好的例子就是std::swap函数

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

这里没有使用右值,因此有没有实现移动语义,但是我们知道实现移动语义会更好,只要变量作为复制构造或者赋值的源出现,该变量要么根本就不再使用,要么就作为赋值的目标。

C++11中与一个被调用的库函数std::move可以将其参数转换 右值, 不做其他事情

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);

修改之后上述三行实现了移动语义,需要注意的是,对于那些没有实现移动语义的类型(即:没有使用右值引用版本重载它们的复制构造函数和赋值运算符),对于这些类型新的swap就和旧的一样

既然、知道了移动语义std::move,如下:

a = b;

你期望在这里发生什么?你期望a持有的对象被b的复制出来的副本替换,并且希望a先前持有的对象析构,现在我们考虑一下语义:

a = std::move(b);

如果实现了移动语义,会交换a和b持有的对象,不会有任何对象进行析构。当然结束之后a原先持有的对象的生命周期将和b的作用范围绑定,b超出范围a原先持有的对象将会被销毁。

所以从某种意义上说,我们在这里陷入了非确定性破坏的阴暗世界:一个变量已被分配,但该变量以前持有的对象仍在某处。只要该对象的销毁不会产生任何外界可见的副作用,就可以了。但有时析构函数确实有这样的副作用。一个例子是释放析构函数内的锁。因此,具有副作用的对象销毁的任何部分都应该在复制赋值运算符的右值引用重载中显式执行:

X& X::operator=(X&& rhs)
{// Perform a cleanup that takes care of at least those parts of the// destructor that have side effects. Be sure to leave the object// in a destructible and assignable state.// Move semantics: exchange content between this and rhsreturn *this;
}

右值引用就是右值吗?

像以前一样,我们为X实现复制构造函数和赋值操作符重载来实现移动语义。

假如:

void foo(X&& x)
{// x是右值引用,但是x本身是一个左值,以为x是有命名的X anotherX = x; //  调用右值引用赋值重载函数还是左值???// ...
}

代码中函数内x是一个左值引用,然而我们期望让右值引用就是本身就是右值。右值引用的设计者提供了一个更好的思路:

Things that are declared as rvalue reference can be lvalues or rvalues. The distinguishing criterion is: if it has a name, then it is an lvalue. Otherwise, it is an rvalue.

大意就是,右值引用可以是左值也可以是右值,评判的标准是,如果这个值有命名就是左值,如果没有就是右值。

那么上述代码中,虽然参数传进的是右值,但是进入函数的时候,因为x已经有命名了,所以函数内部的x是左值,那么函数内部调用的也是左值的赋值函数

void foo(X&& x)
{X anotherX = x; // calls X(X const & rhs)
}

如下是一个没有名字的右值,因此会调用右值赋值函数

X&& goo();
X x = goo(); // calls X(X&& rhs) because the thing on// the right hand side has no name

这种设计的背后思路就是:允许移动语义应用于一些有名字的对象

X anotherX = x;// x is still in scope!

以上语句是非常危险的,移动的食物应该在移动后立即死亡并消失,因此有一条规则,如果它有一个名字,那么它就是左值

如果没有名字,那么他就是个右值,如果有名字需要使用std::move()进行转换,std::move()通过将其参数转换为右值,即使这个这个参数不是右值。

在编程的过程中,时刻注意变量是否有一个名字,也就是注意变量是否是右值非常的重要。

假设你实现了一个基类,当然为了实现移动语义你要给基类实现复制构造函数和复制操作符重载

Base(Base const & rhs); // non-move semantics
Base(Base&& rhs); // move semantics

现在假设已实现了一个类Derived继承了Base基类,为了确保Derived类中继承的Base也实现了移动语义,你必须实现Derived的复制构造函数和赋值操作符,我们先看下复制构造函数的重载

Derived(Derived const & rhs) : Base(rhs)
{// Derived-specific stuff
}

可以看到很简洁,只需要将传递个Derived的参数,复制构造的时候传递给Base就可以了,那我们来看下移动复制构造函数的实现:

Derived(Derived&& rhs) : Base(rhs) // wrong: rhs is an lvalue
{// Derived-specific stuff
}

如果我们这样调用,那么将会调用基类的非移动语义的复制构造函数,因为rhs传递给Base的时候,是有名字的,所以是按照左值传递的。如果我们想按照移动语义进行调用,我们可以按照如下的方式实现:

Derived(Derived&& rhs) : Base(std::move(rhs)) // good, calls Base(Base&& rhs)
{// Derived-specific stuff
}

移动语义和编译优化

考虑到有如下函数定义:

X foo()
{X x;// perhaps do something to xreturn x;
}

现在想象一下,我们通过对X类重载复制构造函数和赋值操作符实现了移动语义,如果你只从表面上来看,上述代码中x变量,在进行return的时候,会存在值得复制,就是局部变量x复制给返回值,来让我们使用移动语义优化一下吧:

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

实际上这样写之后,会使事情变得比以前更糟。因为现代的编译器都会实行返回值优化(RVO),换句话说,比起构造一个局部的x对象,并将其复制出去,编译器更倾向于直接构造返回值并将其按照引用的方式在函数内部使用。

例如:

class X {public:X() {cout << "Construct X " << endl;}~X() {cout << "Destruct X" << endl;}
};X ReturnValueOptimization() {X x;return x;
}int main(int argc, char* argv[]) {X retValue = ReturnValueOptimization();return 0;
}

函数执行返回

$ ./return_value_optimization
Construct X
Destruct X

如果没有执行返回值优化,那么按照代码字面的意思,正常的应该是要进行两次构造和析构,但是从执行结果可以看出,实际上只执行了一次构造和析构函数,因此在有返回值的情况下,现在的编译器都会对其进行返回值优化

所以为了正确的使用右值和移动语义,你需要充分考虑当今编译器的特殊效果。例如返回值优化和复制省略等;

完美转发的问题

另外一个需要基于右值引用实现的移动语义来进行解决的问题就是完美转发,看下如下函数:

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

显然函数的目的是为了实现从factory函数将参数完美转发给T的构造函数,理想的情况下,应该能够实现就像外层的factory函数不存在一样,并且构造函数能够直接调用用户传进来的参数–这就是完美转发。上面函数的问题就是factory函数按照值进行参数传递,更坏的情况是如果T的构造函数按照引用用参数,那么将带来严重的后果。

好一点的改进就是,factory函数按照引用来进行参数传递,如boost::bind函数一样

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

改进之后相对于按照值传递好一点,但是并不是很完美,问题在于factory函数不能按照右值调用。

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

当然,这样的问题能够使用增加const修饰符来解决:

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

当然使用const还是会存在问题,首先如果factory不止有一个参数,你必须为所有参数的组合提供函数的const重载,因此对于多个参数的解决方案非常的有限。

其次,这种转发并不够完美,因为factory函数内的arg是一个左值,因此移动语义不可能发生,即使没有factory函数题也不会发生

最后,我们可以使用移动语义解决上述的两个问题,移动语义可以实现真实意义上的完美转发,为了了解如何实现,我们需要知道另外两个右值引用的规则:

完美转发的解决方式

第一条:右值引用的规则,同样也会影响左值引用。C++11之前不允许使用引用的引用,如果A& &将会造成编译错误,C++11中中引入了一下折叠规则:

  • A& & --> A&
  • A& && --> A&
  • A&& & --> A&
  • A&& && --> A&

第二条: 对于使用右值引用的模板函数,有一个特殊的模板推倒规则

template<typename T>
void foo(T&&);
  1. 当使用类型为A的左值调用foo函数的时候,根据折叠规则,参数将变成A&
  2. 当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)));
}
where std::forward is defined as follows:template<class S>S&& forward(typename remove_reference<S>::type& a) noexcept
{return static_cast<S&&>(a);
}

不用关注noexcept,它只是为了告诉编译器编译优化意图,该函数不会抛出任何异常。

假设:factory函数被一个类型为X的左值调用:

X x;
factory<A>(x);

然后根据折叠规则,factory的模板参数Arg将会被解析成X&,编译器将会生成如下的factory函数和std::forward

shared_ptr<A> factory(X& && arg)
{ return shared_ptr<A>(new A(std::forward<X&>(arg)));
}
X& && forward(remove_reference<X&>::type& a) noexcept
{return static_cast<X& &&>(a);
}

经过折叠之后

shared_ptr<A> factory(X& arg)
{ return shared_ptr<A>(new A(std::forward<X&>(arg)));
}
X& std::forward(X& a)
{return static_cast<X&>(a);
}

这样左值也实现了完美转发,工厂函数经过两次间接传递,将参数arg传递给构造函数,并且是通过老式的左值引用。

现在我们假设工厂函数factory被一个类型为X的右值调用:

X foo();
factory<A>(foo());

经过折叠规则之后factory函数将如下:

shared_ptr<A> factory(X&& arg)
{ return shared_ptr<A>(new A(std::forward<X>(arg)));
}
X&& forward(X& a) noexcept
{return static_cast<X&&>(a);
}

该模板函数实现了对右值的完美转发,经过两次引用传递之后A的构造函数还是拿到了右值的arg参数,并且经过forward转发之后,A构造函数拿到的变量没有名字。因此根据无"无名"规则,该变量就是一个右值。因此将调用A的右值构造函数

接下来让我们看下std::move(),该函数知识实现了将传给其的参数转绑定成像右值一样,如下是其实现:

template<class T>
typename remove_reference<T>::type&&
std::move(T&& a) noexcept
{typedef typename remove_reference<T>::type&& RvalRef;return static_cast<RvalRef>(a);
}

假设我们向std::move()传递一个类型为X的左值:

X x;
std::move(x);

根据新的模板推导付规则,模板参数T会被解析成X&,因此编译器最终实例化是:

typename remove_reference<X&>::type&&
std::move(X& && a) noexcept
{typedef typename remove_reference<X&>::type&& RvalRef;return static_cast<RvalRef>(a);
}

经过参数折叠和remove_reference的作用之后,会生成如下代码:

X&& std::move(X& a) noexcept
{return static_cast<X&&>(a);
}

到这里std::move()所实现的事情就一目了然了,就是接收左值引用,并将其转换为无明明的右值引用。

右值引用和异常

当你采用C++进行代码开发时,是否使用在你的代码里使用异常处理是你决定的事情,但是右值引用比较特殊。当你为一个类实现移动语义,进行复制构造函数重载和赋值函数实现的时候,必须要遵循一下规则:

  1. 确保重载的方式不会引发异常,因为移动语义通常只是在两个资源之间交换指针和资源句柄
  2. 如果你成功的实现了不抛出异常的重载,要确保在函数上加上noexcept关键字

如果你没有实现上述两种规则,那么至少一种常见的场景下是不能使用你定义的移动语义的:当一个std::vector()被调整大小的时候,你希望调整大小的时候能发生移动语义,但是除非你实现以上两条规则否则移动语义在这里不会发生-- Effective Modern C++条

隐式移动

在右值引用的复杂讨论中,标准委员会提出移动构造函数、移动赋值操作符,编译器应当自动生成-在用户没有提供的情况下。这看起来很正常,因为编译器会在用户没有提供的情况下,自动的提供构造函数和赋值操作符的默认实现。但是Scott Meyers向编译器提交了一个消息posted a message on comp.lang.c++,里面详细论述了如果编译器提供移动语义的构造函数和赋值操作符的实现,将会对以前已经存在的代码引入一个非常严重的问题。当然也可以参考Scott Meyers的Effective Modern C++的地17条

推荐:
C++右值引用
Implicit Move Must Go

A Brief Introduction to Rvalue References

Implicit Move Must Go
C++ Rvalue references Explained

移动语义-右值引用-完美转发-万字长文让你一探究竟相关推荐

  1. C++右值引用和完美转发

    C++右值引用和完美转发 何为引用 引用必须是左值 右值引用 完美转发 move() 使用move的优点 move 左值测试 move 右值测试 注意 参考链接 看到有些同学,调用函数的时候总喜欢使用 ...

  2. C++11 右值引用和移动语义

    C++11 右值引用和移动语义 右值引用 左值与右值 对象的返回形式缺陷 ★移动语义 右值引用引用左值(move) 正确使用move的一个例子 完美转发 转发: 不转发: 右值引用作用 右值引用 C+ ...

  3. 深入浅出C++左值引用,右值引用,移动语义。

    什么是左值 右值? 简单来说左值就是可以取地址,在=左边的,而右值就是不可以取地址,在=右边的. int t=10; t可以通过&取地址在=左边 所以t是左值 10不可以取地址 在=右边10是 ...

  4. [译]详解C++右值引用

    2019独角兽企业重金招聘Python工程师标准>>> C++0x标准出来很长时间了,引入了很多牛逼的特性[1].其中一个便是右值引用,Thomas Becker的文章[2]很全面的 ...

  5. 的引用_左值、右值、左值引用、右值引用

    [导读]:本文主要详细介绍了左值.右值.左值引用.右值引用以及move.完美转发. 左值和右值 左值(left-values),缩写:lvalues 右值(right-values),缩写:rvalu ...

  6. 深入理解右值引用,move语义和完美转发

    move语义 最原始的左值和右值定义可以追溯到C语言时代,左值是可以出现在赋值符的左边和右边,然而右值只能出现在赋值符的右边.在C 里,这种方法作为初步判断左值或右值还是可以的,但不只是那么准确了.你 ...

  7. 可变参数模板、右值引用带来的移动语义完美转发、lambda表达式的理解

    可变参数模板 可变参数模板对参数进行了高度泛化,可以表示任意数目.任意类型的参数: 语法为:在class或者typename后面带上省略号. Template<class ... T> v ...

  8. C++11 右值引用、移动语义、完美转发、万能引用

    C++11 右值引用.移动语义.完美转发.引用折叠.万能引用 转自:http://c.biancheng.net/ C++中的左值和右值 右值引用可以从字面意思上理解,指的是以引用传递(而非值传递)的 ...

  9. C++11右值引用、移动语义、完美转发详解

    c++中引入了右值引用和移动语义,可以避免无谓的复制,提高程序性能.有点难理解,于是花时间整理一下自己的理解. 左值.右值 C++中所有的值都必然属于左值.右值二者之一.左值是指表达式结束后依然存在的 ...

最新文章

  1. 双向链表的插入和删除算法描述
  2. STM32 单片机启动流程
  3. ping C类地址是否在线
  4. spdk-nvmf指南
  5. [转]简单介绍如何用Reporting Service制作报表
  6. RavenDb中的Task异步应用.Net4
  7. python propresql mysql_python数据库操作mysql:pymysql、sqlalchemy常见用法详解
  8. 【转】Serverless架构
  9. idea中resources下的logback-spring的配置
  10. java音频播放(转)
  11. 7.Linux性能诊断 --- 分布式追踪系统体系概要
  12. 显式积分,隐式积分和弹簧质点系统(详细公式推导和太极源码)
  13. 110kV变电站电气一次系统设计
  14. 如何防止你的网站被攻击?
  15. 后端/Java/大数据/C++ 校招内推面经
  16. 远程关闭计算机提示拒绝访问权限,如何解决shutdown远程关机win10拒绝访问的问题...
  17. SQLTracker跟踪工具用法
  18. linux 6安装EBS R12.2 Post-Install Check : RW-50016: Error: - {0} was not created
  19. 在VMware实验Ubuntu虚拟机的使用
  20. 2020年系统集成项目管理工程师考试目标及要求

热门文章

  1. DTMF--VAD 项目分析
  2. java IO(输入输出) 对象的序列化和反序列化
  3. NYOJ 372 巧克力
  4. NYOJ 659 判断三角形
  5. NYOJ 228 士兵杀敌(五)
  6. NYOJ 833 取石子(七)
  7. Tkinter模块常用参数(python3)
  8. 枚举是如何实现的?(枚举的线程安全性及序列化问题)
  9. 获取jar包内部的资源文件
  10. QWT中Qdial的入门介绍