Effective Modern C++ 学习笔记

发布时间 2023-05-10 09:28:35作者: imxiangzi

前言
记录下阅读此书的感想与总结,一方面能巩固复习,另一方面也能更好地浓缩本书的精华,方便日后的回看。

第五章 右值引用、移动语义和完美转发
它们带来的好处

移动语义使得编译器能使用效率更高的移动操作来替换昂贵的复制操作
移动语义使得创建只移对象成为可能,如:std::unique_ptr, td::thread ,std::future 等
完美转发使人们可以撰写接受任意实参的函数模板,并将其以它应该的形式转发到其他函数
条款23 理解 std::move 和 std::forward
学完这个条款,应能对它们的作用有个认知

这两者在运行期间什么都不做,它们不会真正地进行移动、转发,只是在编译期间负责类型的强制转换。

std::move
它只做一件事:把实参强制转换为右值。右值是可以移动的,所以std::move相当于告诉编译器对象具备可移动的条件。
考虑下面这个例子

class Annotation {
public:
explict Annotation(const std::string text)
: value(std::move(text))
{ ... }
...
private:
std::string value;
};
1
2
3
4
5
6
7
8
9
这段代码顺利完成编译,value在初始化时也确实接受到右值,然而最终调用的会是复制而非移动操作,理由如下:
我们的 text 属性是 const std::string,经过移动其仍然带有 const 属性,在匹配时编译器当然会将其匹配到拷贝构造函数以保证其常量性不会消失。

class string {
public:
string(const string & rhs);
string(string && rhs);
...
};
1
2
3
4
5
6
结论:如果我们想要移动某个对象,则不要将其声明为 const

std::forward
考虑一个例子

void proccess(const Widget& lvalArg);
void process(Widget&& rvalArg);

template<typename T>
void logAndProcess(T&& param) {
...
process(std::forward(param));
}

Widget w;
logAndProcess(w); //我们希望调用左值版本
logAndProcess(std::move(w)); //我们希望调用右值版本
1
2
3
4
5
6
7
8
9
10
11
12
然而 param 是形参,其一定是个左值,如果没有 std::forward,process 一定会调用左值版本。这时使用 std::forward 转发即可得到正确结果。
std::forward 只做一件事:仅在 param的实参 为右值的情况下把 param 转换成右值类型。换言之,它保留了对象的左值性与右值性,该是什么就是什么。

条款24 区分万能引用与右值引用
学完这个条款,应能辨别万能引用与右值引用

万能引用作用:
首先是个引用,其对应一个初始化物。
如果初始化物是左值引用,则万能引用对应到一个左值引用
如果初始化物是右值引用,则万能引用对应到一个右值引用

template<typename T>
void f(T&& param);
Widget w;
f(w); //param 是 Widget&
f(std::move(w)); //param 是 Widget&&
1
2
3
4
5
一些右值引用和万能引用的例子

Widget&& var1 = Widget(); //右值引用
auto&& var2 = var1; //万能引用

template<typename T>
void f(std::vector<T>&& param); //右值引用

template<typename T>
void f(T&& param); //万能引用
1
2
3
4
5
6
7
8
关键看该引用是否真的涉及到类型推导,并且其类型必须形如 T&&

template<typename T>
void f(std::vector<T>&& param);
template<typename T>
void f(const T&& param);
1
2
3
4
这个例子中, param 类型为 std::vector<T> / const T&& 不为T&& 故不为万能引用
即使形式对了,还需真的满足类型推导

template<class T, class Allocator = allocator<T>>
class vector {
public:
void push_back(T&& x);
...
};
1
2
3
4
5
6
这个例子中, push_back 作为 vector 的一部分,只有当 vector 实例化,其才会存在,实例化后,T的类型就已经确定,它自然就不是万能引用了。

条款25 针对右值引用实施std::move,针对万能引用实施std::forward
学完这个条款应能正确地使用它们

这个条款必须遵守,没有多少余地,不遵守很可能会出错,理由是很平凡的:
如果对万能引用使用 std::move()时, 则我们保证我们不会再使用这个初始化物了(这是因为这个初始化物会被 move 成 右值,而右值是将亡值。)这就意味着,万能引用的初始化物必须是右值才能有这样的保证。这是不对的。

针对右值引用的最后一次使用,使用 std::move ,针对万能引用的最后一次使用,使用 std::forward

template<typename T>
void setSignText(T&& text) {
sign.setText(text); //使用text
//但不改其值
...
sighHistory.add(now, std::forward<T>(text)); //转换
}
1
2
3
4
5
6
7
在按值返回的函数中,如果返回的是一个绑定到右值引用或万能引用的对象,则返回时,请使用 std::move / std::forward
考虑一个例子

Matrix operator+(Matrix&& lhs, const Matrix& rhs) {
lhs += rhs;
return std::move(lhs);
}
Matrix operator+(Matrix&& lhs, const Matrix& rhs) {
lhs += rhs;
return lhs;
}
1
2
3
4
5
6
7
8
毫无疑问,上面的版本比下面的版本更好,如果 Matrix 有移动构造函数,则上面的版本将使用移动而非复制操作。如果Matrix很大,则效率会有较大差别。其次,就算Matrix 没有移动构造函数,上面的版本也会使用复制构造函数,与下面的版本达到相同的效果。所以,没有理由不使用 std::move

但是考虑对局部变量的优化时,则全然不同

Widget makeWidget() {
Widget w;
...
return w;
}
// 请不要使用下面的版本!!
Widget makeWidget() {
Widget w;
...
return std::move(w);
}
1
2
3
4
5
6
7
8
9
10
11
这里涉及到的是编译器的**返回值优化(RVO)**操作:直接在为函数返回值分配的内存上创建局部变量w来避免复制之。(有点像 STL 里的 emplace_back 就地构造而非 移动 / 复制)

RVO要满足两个条件

局部对象类型与函数返回值类型相同
返回的就是局部对象本身
而上面的第二个版本返回的是一个右值引用,不满足条件2,因此我们限制了编译器的优化。并且就算编译器禁用了RVO操作,我们仍无需加std::move
因为标准要求如果实施RVO的条件满足但没有实施RVO(如被禁用)的话,返回对象必须作为右值处理,这就意味着编译器会隐式帮我们加上 std::move

条款26 避免依万能引用型别进行重载
直接照做
这个条款的理由是:万能引用几乎总能精确匹配类型,所以函数几乎不会像我们预想的那样被重载

条款27 熟悉依万能引用型别进行重载的替代方案
我的建议是条款26、27能大致看懂书的内容即可,不用深究

条款28 理解引用折叠
学完这个条款,应当理解前面机制的底层理由
引用折叠,就是引用的引用,虽然我们被禁止声明,但编译器可以在特殊时刻产生引用的引用。
规则如下:

如果任一引用为左值,则结果为左值引用,否则(两个皆为右值引用)结果为右值引用。
template<typename T>
void f(T&& param);

Widget w;
f(w); //T的推导结果为Widget&
//T & && = T& 所以传递了左值引用
f 被实例化为 f(Widget& param);
1
2
3
4
5
6
7
这里忘记 T 为什么被推导为 Widget& 的话,请回看 条款1

我们再加上 std::forward

//std::forward的一种简单实现
template<typename T>
T&& forward(typename remove_reference<T>::type& param) {
return static_cast<T&&> param;
}

template<typename T>
void f(T&& param){
...
someFunc(std::forward<T>(param));
}
1
2
3
4
5
6
7
8
9
10
11
回忆:
std::forward 只做一件事:仅在 param的实参 为右值的情况下把 param 转换成右值类型。
如果我们传给 函数f 一个左值, T 被推导为 Widget& ,然后 std::forward<T> 变为 std::forward<Widget&>,代入上面的 forward 实现

Widget& && forward(typename remove_reference<Widget&>::type& param) {
return static_cast<Widget& &&> param;
}
//变为
Widget& forward(Widget& param) {
return static_cast<Widget&> param;
}
我们成功得到左值的param
1
2
3
4
5
6
7
8
如果我们传给 函数f 一个右值, T 被推导为 Widget ,然后 std::forward<T> 变为 std::forward<Widget>

Widget&& forward(typename remove_reference<Widget>::type& param) {
return static_cast<Widget&&> param;
}
//变为
Widget&& forward(Widget& param) {
return static_cast<Widget&&> param;
}
我们成功得到右值的param
1
2
3
4
5
6
7
8

————————————————
版权声明:本文为CSDN博主「今天要努力打游戏」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_70354558/article/details/130344908

 

 

https://blog.csdn.net/weixin_70354558/article/details/130344908