type
status
date
slug
summary
tags
category
icon
password
Property
新标准的一个特色就是可以移动对象而不是拷贝对象。当对象一旦拷贝完就销毁时,移动而不是拷贝对象将提供重大的性能提升。有一些类的资源是不可共享的,这种类型的对象可以被移动但不能被拷贝,如:
IO
或unique_ptr
。早期版本中,并没有什么方式可以直接移动对象。即便是在不需要拷贝的时候依然会执行拷贝。同样,在之前存储在容器中的对象必须是可拷贝的,在新标准中,可以在容器中使用不可拷贝但是可以移动的对象。
库容器、
string
和shared_ptr
支持拷贝和移动,IO
和unique_ptr
则只能移动不能拷贝。左值引用VS右值引用
左值是一个表示数据的表达式(如变量名或解引用的指针),有如下特性:
- 可以获取它的地址+可以对它赋值(不一定能赋值,但一定能取地址)
- 左值可以出现赋值符号的左边,右值不能出现在赋值符号左边
- 定义时
const
修饰符后的左值,不能给他赋值,但是可以取它的地址
左值引用就是给左值的引用,给左值取别名:
右值也是一个表示数据的表达式,如临时变量:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回,要是传值返回)等等,有如下特性:
- 右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边
- 右值不能取地址
C++
里又把右值分为两类(纯右值和将亡值):- 纯右值(内置类型的对象):10、a + b……
- 将亡值(自定义类型的对象):传值返回生成的拷贝
:to_string(1234)
、匿名对象:string("11111")
、s1 + "hello"
左值和右值最大区别在于左值可以取地址,右值不可以取地址。因为右值是临时变量,没有实际被存储起来。
为了支持移动操作,新标准引入了一种新的引用称之为右值引用(rvalue reference)。右值引用是必须绑定到右值的引用,右值引用使用
&&
符号,相较于左值引用的&
。右值引用有一个特性就是其只能绑定到即将销毁的对象上,因而,可以自由的移动右值引用对象中的资源。右值引用就是对右值的引用,给右值取别名:
左值表示对象的身份,而右值表示对象的值。不能将左值引用(lvalue reference)绑定到需要转型的值、字面量或者返回右值的表达式上。右值引用则刚好相反:可以将右值引用绑定到以上的值,但不能直接将右值引用绑定到左值。
返回左值引用的函数和赋值、下标操作、解引用和前缀自增/自减操作符都是返回左值的表达式,可将左值引用绑定到这些表达式的结果中。
返回非引用类型的函数与算术、关系、位操作和后缀自增/自减的操作符都是返回右值的表达式,可将
const
左值引用和右值引用绑定到这种表达式上。- 左值引用只能引用左值,不能引用右值,但是
const
左值引用既可以应用左值,也可以引用右值
- 右值引用只能引用右值,不能引用左值,但是右值引用可以引用
move
以后的左值。
左值持久右值短暂
左值具有持久的状态,而右值要么是字面量,要么就是临时量。由于右值引用只能绑定到临时量上,可知其绑定的对象是即将被销毁的,对象是被独占的,没有其它对象使用它。这些事实告诉我们使用右值引用的代码可以自由的移动右值引用所绑定对象的资源。
右值引用绑定即将被销毁的对象,因而,可以从右值引用所绑定对象中自由的移动资源。
变量是左值
一个变量就是一个表达式,其只有一个操作数而没有操作符。变量表达式是左值。因而,不能将右值引用绑定到一个定义为右值引用的变量上:
一个变量就是一个左值;不能直接将右值引用绑定到一个变量上,即使这个变量被定义为右值引用类型也不可以。
move库函数
可以显式将左值强转为对应的右值引用类型,也可以通过调用
move
库函数来获取绑定到左值的右值引用,其被定义在utility
头文件中:调用
move
告知编译器,以右值方式对象一个左值。特别需要了解的是调用move
将承诺:不会再次使用rr1
,除非是赋值或者析构。当调用了move
之后,不能对这个对象做任何值上的假设。可以析构或赋值给移动后的对象,但在此之前不能使用其值。使用
move
的代码应该使用std::move
,而不是move
,这样做可以避免潜在的名字冲突。左值引用的使用场景
左值引用解决的是拷贝构造引发的深拷贝而带来的开销过大、效率低的问题:
- 左值引用做参数,防止传值传参引发的拷贝构造问题(导致效率低)
- 左值引用做返回值,防止返回对象发生拷贝构造的操作(导致效率低)
string
类的+=运算符是左值引用作为返回值,这样做避免了传值返回引发的拷贝构造,而这样做的原因在于string
类的拷贝构造为深拷贝,要经历开空间等操作,开销太大了,导致效率低,传值传参同样也是会发生拷贝构造(深拷贝)这个问题,为了避免如此之大的开销,使用左值引用可以很好的解决此问题,因为左值引用就是取别名,无开销,提高了效率。
左值引用可以避免一些不必要的拷贝构造操作,但是并不是所有情况都是可以避免的:
- 左值引用做参数,能够完全避免传参时不必要的拷贝操作
- 左值引用做返回值,并不能完全避免函数返回对象时不必要的拷贝操作
当函数返回的是一个临时对象时,不能使用引用返回,因为临时对象出了函数作用域就销毁了,只能使用传值返回,而传值返回难免会引发拷贝构造带来的深拷贝问题,但是无法避免,这就是左值引用的短板:
因为这里的
to_string
是传值返回,所以在调用to_string
的时候一定会调用拷贝构造,而拷贝构造实现的又是一个深拷贝,效率低:如果强硬的把上面的
to_string
实现成左值引用返回,那么又会出现一个问题,str
是临时对象,因为是左值引用返回,所以返回的是str
的别名,把别名作为返回值再区拷贝构造ret
对象,但是临时对象str出了作用域就调用析构函数销毁了,即使能够访问对象的值,但是空间已经不存在了,此时就发生了内存错误。为了解决左值引用的短板,
C++11
引出了右值引用,但并不是简单的把右值引用作为返回值,要对string
进行改造。移动构造函数和移动赋值
为了让自己的类可以执行移动操作,需要定义移动构造函数和移动赋值操作符。这些成员类似于对应的拷贝赋值操作,但是他们将从给定对象中窃取资源而不是拷贝资源。
与拷贝构造函数一样,移动构造函数也有一个引用类型的初始参数。不同于拷贝构造函数的是,这个引用参数在移动构造函数中是一个右值引用。与拷贝构造函数一样,其它的参数必须有默认实参。
除了移动资源,移动构造函数需要保证移动后的对象的状态是析构无害的。特别是,一旦资源被移动后,原始对象就不在指向移动了的资源,这些所有权被转移给了新创建的对象。
与拷贝构造函数不同,移动构造函数并不会分配任何新内存;其将攫取参数中的内存,在此之后,构造函数体将参数中的指针都设置为
nullptr
,当一个对象被移动后,这个对象依然存在。最后移动后的对象将被析构,意味着析构函数将在此对象上运行。析构函数将释放其所拥有的资源,如果没有将指针设置为 nullptr
的,就会将移动了的资源给释放掉。移动操作,标准库容器和异常
移动操作通常不必自己分配资源,所以移动操作通常不抛出任何异常。当写移动操作时,由于其不会抛出异常,应当告知编译器这个事实。除非编译器知道这个事实,它将必须做额外的工作来满足移动构造操作将抛出异常。
通过在函数参数列表后加上
noexcept
,在构造函数时则,noexcept
出现在参数列表后到冒号之间,来告知编译器一个函数不会抛出异常。必须同时在类体内的声明处和定义处同时指定
noexcept
。移动构造函数和移动赋值操作符,如果都不允许抛出异常,那么就应该被指定为noexcept
。告知移动操作不抛出异常是由于两个不相关的事实:第一,尽管移动操作通常不抛出异常,它们可以这样做。第二,有些库容器在元素是否会在构建时抛出异常有不同的表现,如:
vector
只有在知道元素类型的移动构造函数不会抛出异常才使用移动构造函数,否则将必须使用拷贝构造函数;移动赋值操作符
移动赋值运算符执行与析构函数和移动构造函数相同的工作。与移动构造函数一样,如果移动赋值运算符不抛出任何异常,就应该将它标记为
noexcept
。类似拷贝赋值运算符,移动赋值运算符必须正确处理自赋值:移动赋值操作符不抛出异常应当用
noexcept
修饰,与拷贝赋值操作符一样需要警惕自赋值的可能性。移动赋值操作符同时聚合了析构函数和移动构造函数的工作:其将释放左操作数的内存,并且占有右操作数的内存,并将右操作数的指针设为nullptr
。移动源对象必须可析构
移动对象并不会析构那个对象,有时在移动操作完成后,被移动的对象将被销毁。因而,当写移动操作时,必须保证移动后的对象的状态是可以析构的。
StrVec
通过将其指针设置为nullptr
来满足此要求。除了让对象处于可析构状态,移动操作必须保证对象处于有效状态。通常来说,有效状态就是可以安全的赋予新值或者使用在不依赖当前值的方式下。另一方面,移动操作对于遗留在移动后的对象中的值没有什么特别要求,所以,程序不应该依赖于移动后对象的值。
例如,从标准库
string
和容器对象中移动资源后,移动后对象的状态将保持有效。可以在移动后对象上调用 empty
或size
函数,然而,并不保证得到的结果是空的。可以期望一个移动后对象是空的,但是这并不保证。以上
StrVec
的移动操作将移动后对象留在一个与默认初始化一样的状态。因而,这个 StrVec
的所有操作将与默认初始化的StrVec
的操作完全一样。其它类,有着更加复杂的内部结构,也许会表现的不一致。在移动后操作,移动后对象必须保证在一个有效状态,并且可以析构,但是用户不能对其值做任何假设。
合成移动操作
编译器会为对象合成移动构造函数和移动赋值操作符。然而,在什么情况下合成移动操作与合成拷贝操作是十分不同的。
与拷贝操作不同的,对于某些类来说,编译器根本不合成任何移动操作。特别是,如果一个类定义自己的拷贝构造函数、拷贝赋值操作符或析构函数,移动构造函数和移动赋值操作符是不会合成的。作为结果,有些类是没有移动构造函数或移动赋值操作符。同样,当一个类没有移动操作时,对应的拷贝操作将通过函数匹配被用于替代移动操作。
编译器只会在类没有定义任何拷贝控制成员并且所有的非
static
数据成员都是可移动的情况下才会合成移动构造函数和移动赋值操作符。编译器可以移动内置类型的成员,亦可以移动具有对应移动操作的类类型成员。移动操作不会隐式被定义为删除的,而是根本不定义,当没有移动构造函数时,重载将选择拷贝构造函数。当用
=default
要求编译器生成时,如果编译器无法移动所有成员,将会生成一个删除的移动操作。被删除的函数不是说不能被用于函数重载,而是说当其是重载解析时最合适的候选函数时,将是编译错误。- 与拷贝构造函数不同,当类有一个成员定义了自己的拷贝构造函数,但是没有定义移动构造函数时使用拷贝构造函数。当成员没有定义自己的拷贝操作但是编译器无法为其合成移动构造函数时,其移动构造函数被定义为被删除的。对于移动赋值操作符是一样的
- 如果类有一个成员其移动构造函数或移动赋值操作符是被删除的或不可访问的,其移动构造函数或移动赋值操作符被定义为被删除的
- 与拷贝构造函数一样,如果其析构函数是被删除的或不可访问的,移动构造函数被定义为被删除的
- 与拷贝赋值操作符一样,如果其有一个
const
或引用成员,移动赋值操作被定义为删除的
如果一个类定义自己的移动构造函数或移动赋值操作符,那么合成的拷贝构造函数和拷贝赋值操作符都将被定义为被删除的
右值移动,左值拷贝
当一个类既有移动构造函数又有拷贝构造函数,编译器使用常规的函数匹配来决定使用哪个构造函数。拷贝构造函数通常使用
const StrVec
引用类型作为参数,因而,可以匹配可以转为 StrVec
类型的对象参数。而移动构造函数则使用 StrVec &&
作为参数,因而,只能使用非 const
的右值。如果调用拷贝形式的,需要将参数转为 const
的,而移动形式的却是精确匹配,因而,右值将调用移动形式的。右值在无法被移动时进行拷贝
如果一个类有拷贝构造函数,但是没有定义移动构造函数,在这种情况下编译不会合成移动构造函数,意味着类只有拷贝构造函数而没有移动构造函数。如果一个类没有移动构造函数,函数匹配保证即便是尝试使用
move
来移动对象时,它们依然会被拷贝。调用
move(x)
时返回Foo&&
,Foo
的拷贝构造函数是可行的,因为可以将 Foo&&
转为 const Foo&
,因而,使用拷贝构造函数来初始化 z
。使用拷贝构造函数来替换移动构造函数通常是安全的,对于赋值操作符来说是一样的。拷贝构造符合移动构造函数的先决条件:它将拷贝给定的对象,并且不会改变其状态,这样原始对象将保持在有效状态内。
拷贝和交换赋值操作与移动
赋值操作符的参数是非引用类型的,所以参数是拷贝初始化的。根据参数的类型,拷贝初始化可能使用拷贝构造函数也可能使用移动构造函数。左值将被拷贝,右值将被移动。因而,这个移动操作符既是拷贝赋值操作符又是移动赋值操作符。如:
所有五个拷贝控制成员应该被当做一个整体:通常,如果一个类定义了其中任何一个操作,它通常需要定义所有成员。有些类必须定义拷贝构造函数,拷贝赋值操作符和析构函数才能正确工作。这种类通常有一个资源是拷贝成员必须拷贝的,通常拷贝资源需要做很多额外的工作,定义移动构造函数和移动赋值操作符可以避免在不需要拷贝的情况的额外工作。
移动迭代器
在新标准中,定义了移动迭代器适配器。移动迭代器通过改变迭代器的解引用操作来适配给定的迭代器。通常,迭代器解引用返回元素的左值引用,与其它迭代器不同,解引用移动迭代器返回右值引用。调用函数
make_move_iterator
将常规迭代器变成移动迭代器,移动迭代器的操作与原始迭代器操作基本一样,因而可以将移动迭代器传给 uninitialized_copy
函数。值得一提的是标准库没有说哪些算法可以使用移动迭代器,哪些不可以。因为移动对象会破坏原始对象,所以将移动迭代器传给那些不会在移动后访问其值的算法才合适。
慎用移动操作:由于移动后的对象处于中间状态,在对象上调用
std::move
是很危险的。当调用 move
后,必须保证没有别的用户使用移动后对象。谨慎克制的在类内使用
move
可以提供重大的性能提升,在用户代码中使用 move
则更可能导致难以定位的 bug,相比较得到的性能提升是不值得的。在类实现代码外使用
std::move
,必须是在确实需要移动操作,并且保证移动是安全的。右值引用和成员函数
除了构造函数和赋值操作符外提供拷贝和移动版本亦会受益。这种可以移动的成员函数中一个使用
const
左值引用,另一个使用非const
右值引用:可以传递任何可以转换为类型 X 的对象给拷贝版本,这个版本从参数中拷贝数据。只能将非 const 右值传递给移动版本。此版本比拷贝版本更好的匹配非 const 右值(精确匹配),因而,在函数匹配中将是更优的,并且可以自由的从参数中移动资源。
通常上面这种重载方式不会使用
const X&&
和 X&
类型的参数,原因在于移动数据要求对象是非 const 的,而拷贝数据则应该是 const 的。以拷贝或移动的方式对函数进行重载,常用的做法是一个版本使用 const T& 为参数,另外一个版本使用 T&& 为参数。
右值与左值引用的成员函数
有些成员函数是只允许左值调用的,右值是不能调用的,如:在新标准前可以给两个字符串拼接的结果赋值:
s1 + s2 = "wow!";
,在新标准中可以强制要求赋值操作符的左操作数是左值,通过在参数列表后放置引用修饰符可以指示 this
的左值/右值特性:引用修饰符可以是
&
或者 &&
用于表示 this
指向左值或右值。与const
修饰符一样,引用修饰符必须出现在非 static
成员函数的声明和定义处。被 &
修饰的函数只能被左值调用,被 &&
修饰的函数只能被右值调用。一个函数既可以有
const
也可以有引用修饰符,在这种情况下,引用修饰符在 const
修复符的后面:重载和引用函数
可以通过函数的引用修饰符进行重载,这与常规的函数重载是一样的,
&&
可以在可修改的右值上调用,const &
可以在任何类型的对象上调用:当定义具有相同名字和相同参数列表的成员函数时,必须同时提供引用修饰符或者都不提供引用修饰符,如果只在其中一些提供,而另外一些不提供就是编译错误。
完美转发
&&
应用在模板中时,不代表右值引用,而是万能引用,万能引用既能接收左值,也能接收右值。万能引用的作用:
- 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值
- 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力
- 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值
实际传入PerfectForward函数模板的左值和右值均匹配到了左值引用版本的Fun函数,而传入PerfectForward函数模板的const左值和const右值均匹配到了const左值引用版本的Fun函数。
造成此现象的根本原因在于右值被引用后会导致右值被存储到特定位置,这时这个右值可以被取到地址,并且可以被修改,所以在PerfectForward函数中调用Func函数时会将t识别成左值。
forward完美转发在传参的过程中保留对象原生类型属性
想要在传参的过程中保留对象的原生类型属性,就需要用到forward函数:
完美转发的使用场景
为了避免深拷贝带来的开销过大,对
push_back
和insert
函数单独写一个右值引用的版本,同样也要对构造函数写一个右值引用的版本,因为创建节点需要用到节点类的构造函数:虽然这里实现了右值引用版本,但是实际的运行结果依然是深拷贝的,和没写之前的运行结果一模一样,原因如下:
&&应用在模板中时,不代表右值引用,而是万能引用,万能引用既能接收左值,也能接收右值。但是在后续的使用中,会把接收的类型全部退化成左值,既然退化成左值,那么自然会进入后续的深拷贝。
此情况就是典型的完美转发的使用场景,解决办法:需要在传参的过程中保留对象的原生类型属性,就需要用到
forward
函数