简介
移动语义(Move)
移动语义用移动操作替代复制操作,并且给与控制对象移动语义的能力,实现只移对象也成为了可能。
完美转发(Forward)
用户可以编写任意实参的函数模板,模板函数可以接收到完全相同的实参。
type&& 本身就有多种含义,它即表示右值引用,又表示万能引用,Move和Forward也有不同的使用场景。这章主要就是要分清楚它具体的几种含义。
条款23.理解std::move与std::forward
实际上move和forward并没有做任何事情,仅仅是做了强制转换。
move和forward而言唯一的区别是forward仅对原本初始化为右值的左值转换为右值。
void process(const Widget& lvalArg);//左值实现
void process(Widget&& rvalArg);//右值实现
template<typename T>
void logAndProcess(T&& param) {
auto now = std::chrono::system_clock::now();
makeLogEntry("Calling 'process' ", now);
process(std::forward<T>(param));//完美转发
}
虽然我们可以看到我们可以向process传入右值,但是所有函数的形参都是左值,所以我们如果发现初始化param的实参为右值时进行强制转换为右值,这就是完美转发的本质。
在后面的条款中我们会提到为何需要对右值进行Move操作,而对万能引用进行forward操作。
条款24.区分万能引用和右值引用
- 涉及到类型推导则为万能引用
- 其他情况下则为右值引用
类型推导的情况
auto&& var2 = var1;//万能引用
template<typename T>
void f(T&& param);//万能引用
//但是如果并不是纯粹的T&& 那么这个不是万能引用
template<typename T>
void f(std::vector<T>&& param);//右值引用
万能引用可以接受右值或者左值,并且保留其引用类型。
条款25.对右值使用std::move对万能引用使用std::forward
当应该使用move时使用了forward
即使不会有特别打的问题,但是需要多打很多字,也没有必要。
当应该使用forward时使用了move
会导致不应该转为右值的引用变为了右值,导致仍在使用的变量变为不可用的值。
如果添加一个常量左值引用的重载也可以保证move只针对右值使用,但是需要添加额外的重载,参数多了之后会变得非常麻烦。
我们在函数中对一个万能引用使用forward一般是在其他操作结束的情况下,这样可以保证在执行操作的时候该万能引用依旧是可用的。
template<typename T>
void setSignText(T&& text){
sign.setText(text);//不采用forward
auto now = std::chrono::system_clock::now();
signHistory.add(now, std::forward<T>(text));//采用forward
}
在右值需要用作返回值进行返回时,可以在return时使用move操作符减少拷贝消耗。
Matrix operator+(Matrix&& lhs, const Matrix& rhs){
lsh += rhs;
return std::move(lsh);//结果拷贝
}
对于万能引用来说也一样
template<typename T>
Fraction reduceAndCopy(T&& frac){
frac.reduce();
return std::forward<T>(frac);//如果是右值,则移动,否则拷贝
}
但是针对局部变量不要这么做,因为C++标准中编译器会有RVO优化,默认采用移动,如果我们自己添加了移动操作则会导致该优化失效。
Widget makeWidget(){
Widget w;
return std::move(w);//不要这么做
}
条款26.避免依万能引用进行重载
万能引用容易将函数调用吸入其中,导致其他的重载函数无法调用到,所以我们需要尽可能避免将重载和万能引用一起使用。
特别是使用完美转发当做构造函数,会形成比复制构造函数更匹配的形式,并且会劫持鸡肋的复制和移动构造函数。
总之就是别用。
条款27.熟悉万能引用类型进行重载的方法
总的来说有5个方法,两种类型
通过形参修改来避免
- 舍弃重载:给新的函数一个新的名字
- 传递const T&类型的形参,而不是万能引用,效率比较差,但是比较简洁
- 传值,比较反直觉,具体可以参考条款41,当你知道肯定需要复制形参的时候就可以这么做
通过完美转发来避免
- 标签分配
- 使用enable_if来对模板施加限制
标签分派
通过模板,对函数添加标签形参而进行重载
template<typename T>
void logAndAdd(T&& name){
logAndAddImpl(
std::forward<T>(name),
std::is_integral<typename std::remove_reference<T>::type>()//通过模板进行重载分发
)
}
std::string nameFromIdx(int idx);
void logAndAddImpl(int idx, std::true_type){
logAndAdd(nameFromIdx(idx));
}
template<T>
void logAndAddImpl(T&& name, std::false_type){
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
name.emplace(std::forward<T>(name));
}
接受万能引用的模板添加限制
利用enable_if帮助我们禁用一些我们不想用的类型
class Person{
public:
template<typename T, typename = std::enable_if_t<
!std::is_base_of<Person, std::decay_t<T>>::value//禁用Person的子类
&&
!std::is_integral<std::remove_reference_t<T>>::value//禁用int,因为我们已经有相应的处理了
>
>
}
explicit Person(T&& n)
: name(std::forward<T>(n))
{}
explicit Person(int idx)
: name(nameFromIdx(idx))
{}
权衡
一般前一类方法比较简单,但是需要修改形参,效率也不是那么好。
而后一类效率较高,但是易用性较差,在静态编译的时候用static_assert效果会比较好
条款28.理解引用折叠
我们在代码声明中不能声明引用的引用,但是实际上我们将左值传入万能引用的时候就会出现引用的引用的情况。
template<T>
void func(T&& param);
func(w);
//传入左值后
void func(Widget& && param);//这个时候就需要进行引用折叠
结合情况有左值-右值 左值-左值 右值-左值 右值-右值。
如果有任一引用为左值,结果就会使左值。
万能引用的本质也在于此。
auto&& w1 = w;//w为左值
auto&& & w1 = w;//推导结果为左值
auto&& w2 = w;//w为右值
auto && && w2 = w;//推导结果为右值
我们在类中的右值模板,在折叠有可能变成左值,所以很多时候我们需要先进行remove_reference操作获取原类型。
条款29.假定移动操作不存在、成本高、未使用
移动的优点有时候被过度放大了。实际上很多时候移动操作并不比复制廉价。
移动的优点只有在堆分配的时候可以体现出来。
例如堆上分配的数组,或者字符串的SSO优化,都是在堆上分配,移动和复制成本实际是一样的。
只要清楚了解支持移动语义的类型我们才需要去对其进行移动优化。
以下情况下我们都不需要做针对移动的优化:
- 没有移动操作:这时移动操作会自动变为复制操作
- 移动未能更快:对象在栈上分配
- 移动不可用:没有noexcept声明
条款30.熟悉完美转发的失败情况
一般完美转发失败的情况是指,将同样的实参传入到原函数和实际目标函数获得的结果不同。
以下情况下会导致完美转发失败:
- 大括号初始化列表:无法找到对应的形参推导或者找到了错误的推导,都会有问题
- 0和NULL被用于空指针:改为nullptr即可
- 只有声明的整形static const成员(无法取地址):需要在实现文件中声明
- 重载的函数名字和模板:做强制类型转换,指定使用的重载版本
- 位域(由于无法对被拆分的机器字取地址):可以复制其值之后传入
如何规避完美转发失败的情况是我们所需要了解的