Last Updated on 2022年9月27日
这一部分主要介绍左值,右值,引用,拷贝和移动.这些可以说是C++11显著区别于以前的C++(还有C)的特性.本文主要从易用的角度介绍值类型(值类型实际要更多一些).
拷贝与移动的基本定义
- "拷贝"和"移动"是从逻辑层面定义的,在最终的指令层面,只有"拷贝",没有"移动"操作.
- 对于自定义类,拷贝与移动的实现有基本的准则(非硬性),这些准则是:
- 拷贝:不应对源对象做任何修改.
- 移动:可以对源对象做修改,但是源对象被修改后必须可以被赋值,且可以安全的析构.
- 注意,对于移动操作.我们不能对移出后剩余的部分做任何假定,在没有重新赋值前,不应该使用移出后剩下的对象
- 例如:对一个
vector<int> a;
,其中的所有元素都被auto b = std::move(a);
移出后,不能假定a.size()
值为0了.
- 例如:对一个
- 一般而言:
- 对于同时支持拷贝和移动的类型,我们一般认为,移动操作的性能开销应该优于拷贝.
- 对于锁,线程等逻辑上唯一的资源,其拷贝没有意义,只能进行移动.例如,锁对象之间的移动是在转交锁的所有权.
- 对于某个类型,如果拷贝的性能足够好(满足需求),那么应该优先使用拷贝.
- 一个常见的风格: 所有类型都应该是copy-only或move-only的,必要时,可以给move-only的类型添加一个
clone()
.- 使用这种风格编程的主要优势是可以降低心智负担,进而降低维护成本.
- 这种风格认为,如果类型支持拷贝,那么拷贝一定是高效的,否则就应该让它变成move-only.这样一来,只要编译器不报错,那么代码的基本性能就有保障.
左值,右值
- 语言层面的"对象"可以是左值/右值其中一种.
- 从形式上说, 如果某个"对象"只在一个expression中有意义,那么它就是右值,如临时量,字面值常量.
- 左值意味更加持久的生命周期, 右值的生命周期是完全由编译器管理的.
左值引用T&
与右值引用T&&
- 将引用理解为特殊的指针,对于理解引用的行为很有帮助.(大部分编译器的底层实现就是常量指针)
- 将对象绑定到引用时,就是自动取地址并赋值给指针.
- 通过引用访问对象时,会自动的对指针加解引用符号.
- 除此之外,编译器不会做其他操作.
- 大部分时候,当你遇到困惑时,都可以通过将引用换为指针的方式来辅助自己进行分析.例如
- 绑定仅是取地址,所以绑定操作不会触发任何的构造函数
- 引用销毁时只是指针的销毁,不会触发绑定对象的析构函数.
- 将ref又绑定到ref2上,就是指针的拷贝,自然也不会涉及对象的拷贝(移动).
- 左值引用,右值引用是"类型不同的"的指针.
- 以引用形式进行函数传参(返回)时,就是在传入(返回)指针.
- 左值只可能绑定到左值引用,即{
T &
,const T &
}. - 右值只可能绑定到右值引用或
const T &
,即{T &&
,const T &&
,const T &
}.- 注意,右值绑定到
const T &
是一个cast, 并不是精确匹配,也就是说它可能触发隐式类型转换, 例如const Foo & a = Bar()
,等价于const Foo & a = Foo(Bar())
,只要Foo支持从Bar移动构造即可.
- 注意,右值绑定到
- 引用是一种类型,但是C++限制引用必须被初始化,且不能重复绑定,且不能放在容器中.(主要是历史包袱)
- 必须放在容器中时,使用指针会更合理.
- 在涉及模板时, 存在"引用折叠"的特性, 因为推导出的类型
T
可以是引用类型, 这就可能和已有的引用重复.- 含有
&
的都折叠为&
,例如MyT && &
,MyT & &&
,MyT & &
,其整体上的类型和MyT&
等价; - 仅
MyT && &&
折叠为MyT&&
- 含有
- 注意:引用只能绑定到在编译期长度已知的数组,
void f(int(&r)[]);//误,必须有长度
void f(int(&r)[100]);//正
Type array[N]; auto &ref = array;//正,编译器可推断长度
Type array[100]; auto (&ref)[100] = array;//正,手动告知长度
- 在模板中我们可以利用引用推断数组的长度,
template<class T,int N> void f(T(&r)[N]);//由编译器来推断长度
引用作为形参/返回值
-
引用主要是作为函数的形参或返回值类型, 在常规代码中极少用到引用.
- 常规代码中,常常用引用来给对象起别名,这可能会让代码更好看,例如
auto & point_info = *p_full_point_information;
,不但可以让名字变短,还能避免在后续的代码中使用->
.
- 常规代码中,常常用引用来给对象起别名,这可能会让代码更好看,例如
-
在函数重载中的规则如下.
- 右值将精确匹配到
T&&
或const T&&
形参的函数。当这两个函数未定义时,才可能重载到cosnt T &
- 左值对象只能匹配到
T&
或const T&
- 左值对象和右值对象一般都能初始化
T
形参,T
形参和引用形参优先级相同.
- 右值将精确匹配到
-
虽然逻辑上
T&&
,const T&&
,T&
,const T&
都是合法的类型,在实践中中,我们只应该使用const T&
和T&&
.T&
总是应该由T *
替代,强调对象可能被修改.
-
从逻辑上说,
T&&
和T&
形参传入的对象允许被修改.- 一般而言,我们会认为
T&&
型传入的对象一定会被修改,且仅保证修改后的对象仅仅可以安全的析构. T&
型传入的对象是否被修改并不确定- 从编程风格上说,如果传入对象不会被修改,就应该用
const T&
,使用T&&
和T&
意味着传入对象一定会被修改.
- 一般而言,我们会认为
-
const T&
和const T&&
传入的对象不能修改const T&&
仅在成员重载this
等极其冷门的场合中使用,在没有明确的理由时,应优先使用const T&
-
作为函数的返回形参时,习惯上我们常使用
const T&
或T&
- 返回
const T&&
或T&&
的也是理论上可行的,一般只用在模板中.
- 返回
引用相关的static_cast
- 通过
static_cast<T&&>
可以将对象视作右值来处理.- 例如
static_cast<int &&>(a) = 10;
就会触发编译错误,等号左侧是一个右值. - 注:
xxx_cast<yy>(obj)
要求obj
只能是左值表达式
- 例如
std::move(obj)
表达式的效果和static_cast<T&&>(obj)
一致,实现上使用了模板.
引用与拷贝控制
有了之前的背景作为铺垫,我们就可以详细的介绍C++中的拷贝和移动了.
- 对于自定义类
T
T(const T&)
为拷贝构造,T& operator=(const T&)
为拷贝赋值.T(T&&)
为移动构造,T& operator=(T&&)
为移动赋值.
- 因为移动构造函数同时会修改源对象和目标对象,如果因异常中断,移动过程就可能无法恢复现场(导致异常不安全).基于此,标准库在vector等容器中,只当容器元素的移动控制函数为
noexcept
时才会使用移动.