元芳你怎么看

本网站主要用于记录我个人学习的内容,希望对你有所帮助

0%

条款25:针对右值引用实施std::move,针对万能引用实施std::forward

重载?

在条款23的基础上,再来细说一下std::movestd::forward。看这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Data {};
class Entity {
public:
template <typename T>
void setName(T&& newName) {
name = std::move(newName);
}
private:
std::string name;
std::shared_ptr<Data> ptr;
};
std::string getEntityName();
int main() {
Entity e;
auto n = getEntityName();
e.setName(n);
}

看着好像一切安好。注意这里的n是局部变量,传递给e.setName(),调用者假定该函数对n只进行读操作是没有任何问题的。但是setName()内部使用了std::move将其引用的形参无条件强转成右值,因此n的数值就会被移入e.name。运行完setName()后,n就成了一个未知数。

你会说:那我为啥要把形参声明称万能引用自讨苦吃,我直接为其分别重载左值和右值两个版本不就好了。你说的对,但是对的很难受。程序运行效率降低是其次,最主要的是代码难以维护以及扩展性太差。

这个例子只有一个形参,还好处理,两个重载版本就行了。那么10个参数的呢?难不成你重载 (2^{10}) 个版本?有啥想不开的非要这么折磨自己。更别说有些函数模板会有无穷多个形参(比如:std::make_shared

万能引用!

上面的例子修改如下,这样就不需要重载也能实现对应功能了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Data {};
class Entity {
public:
template <typename T>
void setName(T&& newName) {
name = std::forward<T>(newName); // 使用 std::forward 来保持左值或右值性质
}

private:
std::string name;
std::shared_ptr<Data> ptr;
};
std::string getEntityName() {
return "NewName";
}
int main() {
Entity e;
auto n = getEntityName();
e.setName(n);
}

需要注意的点

假设在某个函数中,你刚开始只用了一次该对
象,之后就对其实施std::move或者std::forward,这没有问题,需要注意的是,随着函数不断完善,如果在该函数中不止一次地绑定到右值引用或者万能引用,就会导致上面变量n就成了一个未知数的错误。因此,在这种情况下,只能在最后一次使用该引用的时候对其实施std::move或者std::forward来确保其他操作时候其值依旧存在。

std::move_if_noexcept

std::move_if_noexcept 通常用于优化移动操作,特别是在异常安全性方面。它可以帮助在一些情况下,当移动操作不会抛出异常时,避免不必要的拷贝。

这个函数的典型用例是在容器类的 emplace_backemplace 成员函数中,这些函数通常要求插入元素时提供的参数(构造元素的参数)不会抛出异常。这样可以确保在插入元素时,如果构造过程抛出异常,容器的状态不会受到破坏,因为元素的构造是在容器内完成的。

考虑以下示例,使用 std::move_if_noexceptemplace_back 中:

1
2
std::vector<MyType> myVector;
myVector.emplace_back(std::move_if_noexcept(someValue));

在这里,std::move_if_noexcept 会检查 someValue 是否可以进行无异常移动,如果可以,它会使用移动操作,否则会进行拷贝操作。这有助于确保异常安全性,以避免在构造元素时抛出异常时,不会破坏容器的状态。

要使用 std::move_if_noexcept,您需要确保在需要优化移动操作的情况下,并且对于元素类型的移动构造函数声明了 noexcept 说明符。这通常是在实现自定义类时要考虑的事情。

总之,std::move_if_noexcept 是一种有助于提高性能并确保异常安全性的工具,但需要在适当的情况下使用它。

局部对象可能适用于返回值优化

1
2
3
4
5
6
7
8
9
10
11
12
13
Widget makeWidget ()    // 复制版本
{
Widget w; // 局部变量
// ...
return w; // 将w"复制"入返回值
}
// 将"复制"转换为移动来进行优化?
Widget makeWidget () // 移动版本
{
Widget w;
// ...
return std::move(w); //将w移入返回值,不能这样做!
}

很不幸,这样是不正确的。因为先人就是这样规定的。开个玩笑,因为先人已经比我们领先很多年想到并且解决了这里的优化问题。也就是熟知的RVO(返回值优化)。

编译器如果要在一个桉之返回的函数里忽略对局部对象的复制(或移动),需要满足两个条件:

  1. 局部对象和返回值性别相同
  2. 返回的就是局部对象本身

我们现在往上看“复制版本”的函数,两个条件均满足,所以进行返回值优化,实质上该函数并没有复制任何东西。而移动版本的函数不满足第二个规定,返回的不是本身而是引用,并没有优化,编译器就把返回值w移入函数的返回值存储的位置。RVO的那条福音后面又接着说明,即使实施RVO的前提条件满足,但编译器选择不执行复制省略的时候,返回对象必须作为右值处理。这么一来,就等于标准要求:当RVO的前提条件允许时,要么发生复制省略,要么std:: move隐式地被实施于返回的局部对象上。因此,上面的例子中复制版本的函数就可能被编译器优化成移动的版本。

既然如此,就不要使用std::move或者std::forward来对可能适用于优化的局部对象处理。防止排除编译器RVO的实施机会。