重载?
在条款23的基础上,再来细说一下std::move
和std::forward
。看这个例子:
1 | class Data {}; |
看着好像一切安好。注意这里的n
是局部变量,传递给e.setName()
,调用者假定该函数对n
只进行读操作是没有任何问题的。但是setName()
内部使用了std::move
将其引用的形参无条件强转成右值,因此n
的数值就会被移入e.name
。运行完setName()
后,n
就成了一个未知数。
你会说:那我为啥要把形参声明称万能引用自讨苦吃,我直接为其分别重载左值和右值两个版本不就好了。你说的对,但是对的很难受。程序运行效率降低是其次,最主要的是代码难以维护以及扩展性太差。
这个例子只有一个形参,还好处理,两个重载版本就行了。那么10个参数的呢?难不成你重载 (2^{10}) 个版本?有啥想不开的非要这么折磨自己。更别说有些函数模板会有无穷多个形参(比如:std::make_shared
)
万能引用!
上面的例子修改如下,这样就不需要重载也能实现对应功能了:
1 | class Data {}; |
需要注意的点
假设在某个函数中,你刚开始只用了一次该对
象,之后就对其实施std::move
或者std::forward
,这没有问题,需要注意的是,随着函数不断完善,如果在该函数中不止一次地绑定到右值引用或者万能引用,就会导致上面变量n
就成了一个未知数的错误。因此,在这种情况下,只能在最后一次使用该引用的时候对其实施std::move
或者std::forward
来确保其他操作时候其值依旧存在。
std::move_if_noexcept
std::move_if_noexcept
通常用于优化移动操作,特别是在异常安全性方面。它可以帮助在一些情况下,当移动操作不会抛出异常时,避免不必要的拷贝。
这个函数的典型用例是在容器类的 emplace_back
和 emplace
成员函数中,这些函数通常要求插入元素时提供的参数(构造元素的参数)不会抛出异常。这样可以确保在插入元素时,如果构造过程抛出异常,容器的状态不会受到破坏,因为元素的构造是在容器内完成的。
考虑以下示例,使用 std::move_if_noexcept
在 emplace_back
中:
1 | std::vector<MyType> myVector; |
在这里,std::move_if_noexcept
会检查 someValue
是否可以进行无异常移动,如果可以,它会使用移动操作,否则会进行拷贝操作。这有助于确保异常安全性,以避免在构造元素时抛出异常时,不会破坏容器的状态。
要使用 std::move_if_noexcept
,您需要确保在需要优化移动操作的情况下,并且对于元素类型的移动构造函数声明了 noexcept
说明符。这通常是在实现自定义类时要考虑的事情。
总之,std::move_if_noexcept
是一种有助于提高性能并确保异常安全性的工具,但需要在适当的情况下使用它。
局部对象可能适用于返回值优化
1 | Widget makeWidget () // 复制版本 |
很不幸,这样是不正确的。因为先人就是这样规定的。开个玩笑,因为先人已经比我们领先很多年想到并且解决了这里的优化问题。也就是熟知的RVO(返回值优化)。
编译器如果要在一个桉之返回的函数里忽略对局部对象的复制(或移动),需要满足两个条件:
- 局部对象和返回值性别相同
- 返回的就是局部对象本身
我们现在往上看“复制版本”的函数,两个条件均满足,所以进行返回值优化,实质上该函数并没有复制任何东西。而移动版本的函数不满足第二个规定,返回的不是本身而是引用,并没有优化,编译器就把返回值w
移入函数的返回值存储的位置。RVO
的那条福音后面又接着说明,即使实施RVO
的前提条件满足,但编译器选择不执行复制省略的时候,返回对象必须作为右值处理。这么一来,就等于标准要求:当RVO
的前提条件允许时,要么发生复制省略,要么std:: move
隐式地被实施于返回的局部对象上。因此,上面的例子中复制版本的函数就可能被编译器优化成移动的版本。
既然如此,就不要使用std::move
或者std::forward
来对可能适用于优化的局部对象处理。防止排除编译器RVO
的实施机会。