type
date
status
slug
summary
tags
category
icon
password
网址
C++98中有“左值”、“右值”的概念,C++11以后,又有增加了“纯右值”、“将亡值”的概念。从而引入右值引用,在此基础上实现了移动语义和转发语义,可以避免无谓的复制,提高了程序性能。 1.掌握值类别的概念 2.左值引用与右值引用 3.移动构造和移动赋值 4.右值引用与模板 5.剖析 std::move 原理和使用 6.剖析std::forward 原理和使用 7.剖析emplace 的原理和使用 8.类成员函数的引用修饰
1.类型与值类别: C++ 表达式(带有操作数的操作符、字面量、变量名等)可按照两个独立的属性加以辨别:类型和值类别 (value category)。且每个表达式只属于三种基本值类别中的一种:左值 (lvalue),右值 (rvalue),将亡值(xvalue),每个值类别都与某种引用类型对应。 值类别示意图:
notion image
其中,左值和将亡值合称泛左值,纯右值和将亡值合称右值。
1.1左值(lvalue) 描述: 能够用&取地址的表达式是左值表达式。
1.2纯右值(prvalue)
描述: 本身就是赤裸裸的、纯粹的字面值,如: 3、false, 12.23;
变量a 的类型是int, 变量a可以取地址 &a; 变量a的值类别为左值。 变量b的类型是const int , 变量b可以取地址 &b; 变量b的值类别为左值。 变量dx的类型是double, 变量dx可以取地址&dx, 变量dx的值类别为左值。 指针变量p 的类型是int*, 指针变量p可以取地址 &p; 指针变量p的值类别为左值。 字面常量 10 , 12.23, nullptr;不可以取地址,我们称之为右值,也称为为纯右值。 总结: 所有的具名变量或对象都是左值,而右值不具名。
1.3将亡值:
描述: 在表达式的运行或计算过程中所产生的临时量或临时对象,称之为将亡值;临时量有可能 是字面值,也有可能是一个不具名的对象。
内置类型
算术表达式(a+b、a&b、a<<b)、逻辑表达式(a&&b、a||b、~a)、比较表达式(a==b、a>=b、a<b,a != b)、取地址表达式(&a)等, 计算结果相当于字面值,(数据实际存储在CPU的数据寄存器中),所以是将亡值,此将亡值不可写,由于运行结果是字面量,所以是纯右值。 对 i 加1后再赋给 i ,最终的返回值就是i,所以,++i的结果是具名的,名字就是i,所以是左值。 而对于i++而言,是先对 i 进行一次拷贝(计算过程中所产生的临时量),将得到的副本作为返回结 果,然后再对 i 加 1,由于 i++ 的结果是对 i 加 1 前 i 的一份拷贝,它是不具名,所以是将亡值,此将亡值不可写,所以也是右值。由于运行结果是字面量,所以是纯右值。
类类型
(程序员自己设计的类型)
对象 a 是左值, 可以取地址 &a; Int(2) 程序运行过程中所产生了不具名对象,将亡值。 不可以取地址 &Int(2)错误。
注意: Int(2), Int(3),Int(4) 的生存期问题; 总结: 不具名对象是将亡值,不可以取地址。
函数返回类型是类类型的对象
调用函数,函数返回类型是类类型对象,此对象不具名,将亡值。 注意: 函数返回时建立的将亡值对象的生存期问题。 fun(4) 和fun(5) 有bug;
对比对象的生存期
2.左值引用与右值引用
有左值和右值,就有了左值引用和右值引用.
内置类型 右值引用
具名右值引用自身为左值。
总结: 右值引用 a 是具名右值引用,编译器会将已命名的右值引用视为左值。
右值引用与函数重载
func(a) ; 优先联编 func(int &val) , 如果没有,其次联编func(const int &val); 不能与func(int &&val) 联编。 func(b); 首先联编 func(const int &val) , 不能与func(int &val) 和 func(int &&val) 联编。 func(10); 优先联编func(int &&val); 如果没有,其次联编func(const int &val); 不能与func(int &val) 联编。
函数返回值
总结: func函数返回的数值是将亡值, 不能以左值引用接受。
左值引用与函数返回值。
函数返回值是右值引用
类类型的右值引用
类类型是程序员自己设计的类型
不具名对象
无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又"重获新生",其生命周期与右值引用类型名的生命周期一样,只要该右值引用名还活着,该右值临时量将会一直存活下去。
构造函数的隐式转换
使用explicit 明确关键, 阻止构造函数的隐式转换 。
右值引用与函数重载
类类型作为函数的返回值
 
可以以值的形式接受对象, 也可以const 引用接受对象, 还可以右值引用接受对象. 通过右值引用,比之前少了一次拷贝构造和一次析构,原因在于右值引用绑定了右值,让临时右值的生 命周期延长了.
左值引用作为函数的返回值
分析: 常量左值引用是一个"万能"的引用类型,可以接受左值、右值、常量左值和常量右值。需要注意的是普 通的左值引用不能接受右值.
3.右值引用优化性能,避免深拷贝
浅拷贝和浅赋值
对于含有堆内存的类类型,我们需要提供深拷贝的拷贝构造函数,如果使用默认构造函数, 默认重 载赋值运算符函数(简称赋值函数),会导致堆内存的重复删除,比如下面的代码∶
深拷贝和深赋值
 
移动构造和移动赋值
移动语义是通过右值引用来匹配(将亡值)临时值.
上面的代码中加入了移动构造(Move Construct)和 移动赋值(Move Assignment)。从移动构造函数和移动赋值函数的实现中可以看到,它的参数是一个右值引用类型的参数MyString&&。这里的MyString &&用来根据参数是左值还是右值来建立分支,如果是临时值(将亡值),则会选择移动构造函数。移动构造函数只是将tmp对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高性能。这也就是所谓的移动语义(move 语义),右值引用的一个重要目的是用来支持移动语义的。
移动语言的特点:
1.移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少对堆区的动态内存分配,数据拷贝,以及堆区内存的动态释放,可以大幅度提高C++ 应用程序的性能。 2.消除了对临时对象,对自身资源的维护(创建和销毁),而对性能的影响。
4.右值引用与函数模板
未定的引用类型
上面的例子中有&&,这表示 param 实际上是一个未定的引用类型。这个未定的引用类型称为universal references (可以认为它是一种未定的引用类型),它必须被初始化,它是左值还是右值 引用取决于它的初始化,如果 &&被一个左值初始化,它就是一个左值;如果它被一个右值初始化,它就是一个右值。 需要注意的是,只有当发生自动类型推断时(如函数模板的类型自动推导,或 auto 关键字),&&才是一个 universal references。 1,传递左值或左值引用
x 是 int &, T int &; 2.传递常量左值
 
x 是 const int &, T const int &; 模板实参推演可以带上cv限定符(vc-qualifier, const 和 volatile(易变) 限定符的统称 ). 3.传递右值引用
 
a 是具名右值引用, 编译器会将已命名的右值引用视为左值。 x 是 int &, T 是 int & 转递右值
 
x 是右值引用, T 是int 类型, 不具有引用属性。 在模板编程中 && 总结如下: 1)左值和右值是独立于它们的类型的,右值引用类型可能是左值也可能是右值。 2) 在函数模板中 T&& x是一个未定的引用类型,被称为 universal references,它可能是右值引用,或左 值引用也可能是常左值引用类型,取决于实参的值类型。 3)所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用折叠都为左值引用。当 T&&为模 板参数时,输入左值,它会成为左值引用,输入常量左值,它会成为常量左值引用,而输入右值时则成为 具名的右值引用。 4)编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值。
右值引用变为左值引用
右值引用类型是独立于值的,一个右值引用参数作为函数的形参,在函数内部再转发该参数的时候它已 经变成一个左值了,并不是它原来的类型了。比如:
 
a 是左值 , 调用TestRValue(a) 函数, x被一个左值初始化 , x 是左值; b是常量左值引用, 调用TestRValue(b) 函数, x被一个常量左值初始化 , x 是常量左值; c 是右值引用,已经具有名字, 所以c 是左值, 调用TestRValue(c) 函数, x被一个左值初始化 , x 是左值; 10 字面常量,纯右值, 调用TestRValue(10 ) 函数, x被一个右值初始化 , x 是右值;在TestRValue函数内部 再转发该参数的时候它已经变成一个左值了,并不是它原来的类型了. 编译器会将已命名的右值引用视 为左值,而将未命名的右值引用视为右值。
5.move 语义
我们知道移动语义是通过右值引用来匹配临时值(将亡值,不具名对象)的,那么,普通的左值是 否也能借助移动语义来优化性能呢,那该怎么做呢? C++11为了解决这个问题,提供了std::move 方法来将左值转换为右值,从而方便应用移动语义。 move是将对象中资源的所有权从一个对象转移到另一个对象,只是转移,没有堆内存的申请,拷 贝和释放。深拷贝和 move 的区别如图所示。
move语义示意图:
notion image
在上图中, 对象 SourceObject 中有一个Source资源对象,如果是深拷贝,要将 SourceObject拷贝到 DestObject 对象中,需要将Source拷贝到DestObject 中; 如果是move语义,要将 SourceObject移动到DestObject 中,只需要将 Source 资源的控制权从 SourceObject 转移到DestObject 中,无须拷贝。 move 实际上并不能移动任何东西,它唯一的功能是将一个左值强制转换为一个右值引用,使我们 可以通过右值引用使用该值,以用于移动语义。强制转换为右值的目的是为了方便实现移动构造。 这种 move 语义是很有用的,比如一个对象中有一些指针指向动态数组,在对象的赋值或者拷贝时 当前对象不需要拷贝这些资源,就使用move语义。
移动拷贝和移动赋值
 
再看一个例子,假设一个临时容器很大,赋值给另一个容器。
 
如果不用std::move,拷贝的代价很大,性能较低。使用 move 几乎没有任何代价,只是转换了资源的所有权。实际上是将左值变成右值引用,然后应用move 语义调用移动赋值函数,就避免了拷贝,提高了程序性能。当一个对象内部有较大的堆内存或者动态数组时很有必要写move 语义的拷贝构造函 数和赋值函数,避免无谓的深拷贝,以提高性能。 事实上,STL 中所有的容器都实现了move 语义(移动拷贝和移动赋值),方便我们实现性能优化。 这里也要注意对 move语义的误解,move 只是转移了资源的控制权,本质上是将左值强制转换为右值引用,以用于move语义,避免含有资源的对象发生无谓的拷贝。move 对于拥有形如对内存、文件句柄等资源的成员的对象有效。如果是一些基本类型,比如 int 和char[10]数组等,如果使用move,仍然会发生拷贝(因为没有对应的移动构造函数),所以说move 对于含资源的对象来说更有意义。
std::move 函数与 右值强转(type &&) 的区别:
std::move 函数的实现 :
类模板的部分特化
 
使用using 或 typedef 指定类型别名。
 
加入适配器
加入move函数
MyString
 
6.完美转发和forward
完美转发
因此,我们需要一种方法能按照参数原来的值类型转发到另一个函数,这种转发被称为完美转发。 所谓完美转发(Perfect Forwarding),是指在函数模板中,完全依照模板的参数的值类型(即保持参数的左值、右值特征和const 特征),将参数传递给函数模板中调用的另外一个函数。 C++11中提供了这样的一个函数std::forward,它是为转发而生的,不管参数是T&&这种未定的引用还是明确的左值引用或者右值引用,它会按照参数本来的值类型转发。看看这个例子∶
 
引用叠加:
由于存在T&&这种未定的引用类型,当它作为参数时,有可能被一个左值引用或者右值引用的参数 初始化,这时经过类型推导的T&&类型,相比右值引用(&&)会发生类型的变化,这种变化被称为引 用折叠。C++11中的引用折叠规则如下∶ 1)所有的右值引用叠加到右值引用上仍然还是一个右值引用。 2)所有的其他引用类型之间的叠加都将变成左值引用。 示例:
std::forward 函数的实现
 
7.emplace_back 减少内存拷贝和移动
emplace_back能就地通过参数构造对象,不需要拷贝或者移动内存,相比 push_back 能更好地避免内存的拷贝与移动,使容器插入元素的性能得到进一步提升。在大多数情况下应该该优先使用emplace_back来代替push_back。所有的标准库容器(array除外,因为它的长度不可改变,不能插入 元素)都增加了类似的方法∶emplace、emplace_hint、emplace_front、emplace_after和emplace_back,cppreference.com。这里仅列举典型的示例。
list中emplace_back函数的实现