Last Updated on 2020年4月19日
这一部分主要介绍左值,右值,引用,拷贝和移动.这些可以说是C++11显著区别于以前的C++(还有C)的特性.本文主要从易用的角度介绍值类型(值类型实际要更多一些).
经验谈: 除非”移动”能带来特别明显的性能优势,所有”支持拷贝的自定义类”都应该把移动操作设为
=delete
的.一方面并不是所有人都熟悉移动语义,另一方面,移动语义可能会引入许多bug. 综上,要么类实例是只能拷贝的,要么是只能移动的.
拷贝与移动的基本定义
- “拷贝”和”移动”是从逻辑层面定义的,物理层面只有”拷贝”,没有”移动”操作.
-
对于自定义类,拷贝与移动的实现有基本的准则(非硬性),这些准则是:
- 拷贝:不应对源对象做任何修改.
- 移动:可以对源对象做修改,但是源对象被修改后必须可以被赋值,且可以安全的析构.
- 注意,对于移动操作.我们不能对移出后剩余的部分做任何假定,在没有重新赋值前,不能使用剩下的对象
- 例如:对一个
vector<int> a;
,其中的所有元素都被移出后,不能假定a.size()
值为0了.
- 例如:对一个
- 一般而言:
- 对于涉及资源的类型(如vector),我们一般认为,移动操作的性能开销应该优于拷贝.
- 对于锁,线程等逻辑上唯一的资源,其拷贝没有意义,只能进行移动.例如,锁对象之间的移动是在转交锁的所有权.
- 对于内置类型,物理上只存在拷贝操作.其”移动”只能用拷贝实现.
- 内置类型的”移动”一般仅在编译器为我们合成移动赋值/移动构造时出现.
左值,右值
- 从形式上说,代码中,仅”编译器创建的匿名临时量”和”字面值常量”是右值,其余的对象都是左值.
- 一般情况下,左值意味着我们可以将其放在等号左侧,右值则不行.
const T obj
型的对象只不过在编译期禁用了operator=
.
左值引用T&
与右值引用T&&
- 将引用理解为特殊的指针,对于理解引用的行为很有帮助.(大部分编译器的底层实现就是常量指针)
- 将对象绑定到引用时,就是自动取地址并赋值给指针.
- 通过引用访问对象时,会自动的对指针加解引用符号.
- 除此之外,编译器不会做其他操作.
- 大部分时候,当你遇到困惑时,都可以通过将引用换为指针的方式来辅助自己进行分析.例如
- 绑定仅是取地址,所以绑定操作不会触发任何的构造函数
- 引用销毁时只是指针的销毁,不会触发绑定对象的析构函数.
- 将ref又绑定到ref2上,就是指针的拷贝,自然也不会涉及对象的拷贝(移动).
- 左值引用,右值引用是”类型不同的”的指针.
- 以引用形式进行函数传参(返回)时,就是在传入(返回)指针.
- 左值只可能绑定到左值引用,即{
T &
,const T &
}. - 右值只可能绑定到右值引用或
const T &
,即{T &&
,const T &&
,const T &
}. - 任何类型的”右值”都可能绑定到
const T &
,此时会创建一个临时量,一定要注意.- 例如
const Foo & a = Bar()
,等价于const Foo & a = Foo(Bar())
- 例如
- 引用是一种类型,但是C++限制引用必须被初始化,且不能重复绑定,且不能放在容器中.(主要是历史包袱)
- 如果需要和引用行为相似的对象,目前只能直接使用指针.(或者自定义一个类)
- 引用类型复合时存在”引用折叠”,该特性一般仅用在模板中
- 含有
&
的都折叠为&
,例如T&& &
,T& &&
,T& &
,其整体上的类型和T&
等价; - 仅
T&& &&
折叠为T&&
- 含有
- 注意:引用只能绑定到在编译期长度已知的数组,
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]);//由编译器来推断长度
引用作为形参/返回值
-
引用的作用主要是作为函数的形参,在常规代码中较少用到引用
- 常规代码中创建的引用都会给对象取名,所以通过引用名访问对象时,总是被当做左值处理.
- 为右值对象取名后,右值对象的寿命延长到”名字”不可见为止.
- 在函数重载中的规则如下.
- 右值将精确匹配到
T&&
或const T&&
形参的函数。当这两个函数未定义时,才可能重载到cosnt T &
- 左值对象只能匹配到
T&
或const 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&&)
为移动构造,T& operator=(T&&)
为移动赋值.
- 因为移动构造函数同时会修改源对象和目标对象,如果因异常中断,移动过程就可能无法恢复现场(导致异常不安全).基于此,标准库在vector等容器中,只当容器元素的移动控制函数为
noexcept
时才会使用移动.