1. move和forward函数的效率问题
两个函数本质都是强制转化,都是在编译期完成的,没有任何运行期成本。
2. move函数做了什么事情
move函数无条件地将一个变量转化为其右值引用类型。
3. move的实现
- 从理解角度来说,move就是将任意类型转化为其右值引用。
- 从函数体实现来说,move就是将参数类型强制转化为返回值类型。
1. 形参如何接收任意类型:万能引用
以下是万能引用接收各种类型参数的情况:
template
void move(T&& t);
- 实参是左值引用,解析T为左值引用,t被折叠为左值引用;
- 实参是右值引用,解析T为非引用类型,t被折叠为右值引用;
- 实参是左值(非引用),解析T为左值引用,t被折叠为左值引用。
2. 返回值如何代表任意类型的右值:remove_reference_t
- 引用折叠得到的结果可能是左值和右值,我们需要的只是右值,所以不能使用引用折叠;
- 实现方案是,使用remove_reference类型强制构造出右值来:
template
typename remove_reference::type&& move(T&& t);
3. remove_reference的实现原理:模板及特化
//一般版本:适用于普通左值;
template< class T > struct remove_reference {typedef T type;};
//特化版本:适用于左值引用;
template< class T > struct remove_reference {typedef T type;};
//特化版本:适用于右值引用;
template< class T > struct remove_reference {typedef T type;};
using remove_reference_t = typename remove_reference::type;
总之,remove_reference/remove_reference_t可以保证得到一个类型的非引用类型。
4. 函数体:一个强制转化(强制将参数转化为返回值类型)
template
typename remove_reference::type&& move(T&& t) {
return static_cast::type&&>(t);
}
5. 使用c++14简化实现
template
decltype(auto) move(T&& t) {
using ReturnType = remove_reference_t&&;
return static_cast(t);
}
4. forward做了什么
forward属于条件强制转化,如果模板参数是左值引用,就啥都不做返回,如果模板参数是非引用类型、右值引用,就进行强制转化为右值引用。
5. forward是如何实现的
1. 参数:左值引用
完美转发场景下,模板函数形参一定是右值引用,而在函数内,形参一定是左值引用类型(因为已经可以取到地址)。所以在完美转发场景下,forward拿到的数据类型一定是左值引用。
template
void forward(remove_reference_t& t);
左值引用的实现,同样使用remove_reference。
2. 模板参数的特点
既然forward拿到的数据类型一定是左值引用,那如何知道其原来的类型到底是左值还是右值呢?所以必须手动告诉forward:
//使用forward,必须有模板参数T
template
void func(T&& t) {
return func2(std::forward(t));
}
所以可以认为,forward的模板参数和外层调用的模板参数类型是一致的。完美转发场景下,外层模板参数的类型如上面3-1所示的。
3. 返回值:引用折叠的结果
返回值可能是左值也可能是右值,取决于参数,此时符合引用折叠:
template
T&& forward(remove_reference_t& t);
4. 函数体:一个强制转化(强制将参数转化为返回值类型)
template
T&& forward(remove_reference_t& t) {
return static_cast(t);
}
6. forward理解:完美转发场景下
forward主要用于完美转发场景,从3-1万能引用的解析规则可以看到:
- 完美转发实参是左值或者左值引用,模板参数T被解析为左值引用,传递到forward的模板参数是左值引用,啥都不做返回,所以返回值也是左值引用;
- 完美转发实参是右值引用,模板参数T被解析为非引用类型,传递到forward的模板参数是非引用类型,forward会将参数转化为右值引用。
结论就是,forward完美还原了完美转发函数的实参类型。
7. move函数与const的不兼容性
考虑以下移动语义:
class Widget {
Widget(Widget&& w);
};
Widget w;
Widget w1(std::move(w));
1. 移动语义的使用场景
移动语义比复制语义快的场景是:类内部包含资源(比如指针),移动的时候,就是将资源由一个对象移动到另外一个对象。w1掌管资源后,w不应该再可以访问资源,所以最好将w的指针设置为空,即存在修改w的需求。
----》const&&一般没有使用场景。
2. 函数参数的角度来看const
从参数来看,右值引用没有const修饰。而const作为类型的一部分,一旦实参类型存在const类型,那么肯定不会调用到右值引用的重载版本上来。
3. 结论
实参类型包含const,编译器会选择const引用版本的重载,不会选择右值引用,即使其是一个const&&。
8. move与forward的异同点
1. 场景不同
- move一般在需要右值引用的地方使用;
- forward一般在完美转发的地方使用。
2. 使用方式不同
forward需要显示指定模板参数,如5-2所示。move不需要。
3. forward实现move
根据二者语义,forward的模板参数为非引用类型时,会将形参转化为右值引用返回。所以forward完全可以用来实现move的功能。
string res;
string res1(std::move(res));
string res2(std::forward(res1));
语义上是可以,但是一般不这么使用。
9. move与forward的使用场景
1. move的使用场景
当一个类型实现了移动语义,且这个类型的变量最后一次被使用的时候,才将这个变量使用move返回。因为std::move的使用接下来很可能会改变当前对象。
2. forward的使用场景
完美转发函数内,最后一次使用形参的时候,使用forward修饰。原因和move一样,因为形参可能被转化到右值,然后被修改。
3. 一个应用:return形参
场景描述:当一个函数需要返回的是形参对象,强调不是返回函数内局部变量,无法实行NVO返回值优化。
- 为避免按值返回的拷贝代价,按值拷贝时,一般需要使用std::move;
- 完美转发中,返回的是引用(返回形参的引用没有问题),可以使用forward修饰返回值。
//move的使用
Widget func(const Widget& aaa) {
//use aaa;
...
return std::move(aaa);
}
//forward的使用
template
T&& func(T&& t) {
//use t;
...
return forward(t);
}
- 注意,此时的使用场景不适合返回函数内局部变量。