移动语义使用指南(二)-- 小心std::move 与 vector::push_back 一起使用的坑

目录

问题描述

 三种异常保证

附录


问题描述

Std::move是移动语义特性的一部分。移动语义在c+ 11标准中被引入,目的是提高程序运行的效率,把以前一些需要“先拷贝,再删除源对象”的操作,转化为直接把源对象移动到目标位置。

但是再CppCon 2019的演讲(https://www.youtube.com/watch?v=St0MNEU5b0o&t=2171s  第29分钟)中,Klaus Iglberger提出,使用 vector::push_back 时,假如move constructor没有用noexcept修饰,那么即使使用了std::move ,程序仍然可能采用“先拷贝,再删除源对象”的操作:

采用了noexcept修饰后,程序耗时2毫秒,比不采用noexcept省了4毫秒。原因是不采用noexcept的情况下,push_back不会调用移动构造函数,而是调用拷贝构造函数。

 三种异常保证

原因是什么?答案在David Abrahams的一篇文章里:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2009/n2855.html#throwing-move-constructor

“·········Within the library, we characterize the behavior of a function with respect to exceptions based on the guarantees that the implementation must provide if an exception is thrown. These guarantees describe the state of the program once a thrown exception has unwound the stack past the point of that function. We recognize three levels of exception safety guarantees for a given library function:

Basic exception guarantee

The invariants of the component are preserved, and no resources are leaked.

Strong exception guarantee

The operation has either completed successfully or thrown an exception, leaving the program state exactly as it was before the operation started.

No-throw guarantee

The operation will not throw an exception.

The Standard Library provides at least the basic exception guarantee throughout, which has not changed with the introduction of rvalue references. However, some functions of the library provide the strong exception guarantee, such as push_back (23.1.1p10).“

 

标准库的函数中发生异常时,根据程序能够从异常中复原的程度, (我们当然希望复原的程度越高越好,但是并不是所有的函数都能保证让程序状态100%的复原。有的函数,只能保证恢复到一定程度;有的函数能保证完全恢复。后面的例子会介绍的更具体。) 把函数分成3类。

基本异常保证

组件不变量被保护(文末解释什么是组件不变量),资源不泄露。

强异常保证

假如抛出异常,程序能完全回到函数被调用之前的状态。

无异常保证

操作不会抛出异常。

(可见,这三个保证逐渐变强,可以认为下一个是上一个的子集)

即使在引入右值引用后,STL库的组件至少也能提供基本异常保证。有的STL函数提供的是强异常保证,如vector::push_back。

“……In the call to push_back, if the size of the vector is the same as its capacity, we will have to allocate more storage for the vector. In this case, we first allocate more storage and then "move" the contents from the old storage into the new storage. Finally, we copy the new element into the new storage and, if everything has succeeded, free the old storage.”

调用push_back时,假如vector的实际大小与其容量相同,则vector的内存将重新分配来获取更大空间。这时,首先开辟内存,然后把vector的已有内容“移动”到新内存里。最后把新的元素拷贝到新内存里。假如所有的操作均成功,则释放旧内存。

“……Consider reallocation of the vector when the type stored in the vector provides only a copy constructor (and no move constructor), as shown below. Here, we copy elements from the old storage (top) to the new storage (bottom).”

考虑一个vector,其元素的数据类型只定义了拷贝构造函数,而没有移动构造函数,如下图。这里我们从旧容器(上面),拷贝到新容器(下面)。

“While copying the fifth element (e) the copy constructor throws an exception. At this point, we can still recover, since the old storage still contains the original (unmodified) data. Thus, the recovery code …destroys the elements in the new storage and then frees the new storage, providing the strong exception safety guarantee.

When the type stored in the vector provides a move constructor, each of the values is moved from the old storage into the new storage, potentially mutating the values in the old storage. The notion is shown below, half-way through the reallocation:”

假如在拷贝第五个元素时拷贝构造函数抛出异常,我们仍然可以恢复到拷贝前的状态。这是因为旧的容器里仍然存有元素。只要析构新容器中的元素,然后释放其内存即可(根据《深入理解C++11》第2.6节,析构函数默认是noexcept)。这也就保证了强异常安全。

假如容器中的元素类型提供了移动构造函数,则旧容器中的元素都是移动到新容器中的。这样可能会改变旧容器元素的内容。如下图所示,在填充新容器的中途弹出异常。这时候,想要保持强异常保证就悬了。因为旧的vector中已经失去了abcd四个元素。想要复原,就要把abcd再移动回旧容器。但是移动abcd的过程中又可能抛出异常,所以就不能保证复原成功了。出于上面的考虑,push_back就不再采用移动构造函数。

 

“When the element's move constructor cannot throw, the initialization of the new storage is guaranteed to succeed, since no operations after the initial memory allocation can throw.

However, if the element's move constructor can throw an exception, that exception can occur after the vector's state has been altered (say, while moving the value e), and there's no reliable way to recover the vector's original state, since any attempt to move the previously-moved elements back into the vector's storage could also throw. Hence, the best we can do is maintain the basic guarantee, where no resources are leaked but the first four elements in the vector have indeterminate values.“

除非移动构造函数用noexcept修饰,保证不会抛出异常,此时,push_back才会调用移动构造函数。

译者总结:正因为上面所说,在移动构造函数可能造成异常时,push_back所要求的强异常保证可能做不到。所以,为了确保强异常保证,即使在push_back中用到了std::move,移动构造函数也不会被调用,而是调用拷贝构造函数。

 

附录

何为不变量?

根据https://stackoverflow.com/questions/12137555/strong-exception-guarantee-vs-basic-exception-guarantee 的说法,不变量是编程人员自己定义的。比如说有一个变量代表日期。则其范围就是1到31.超出了这个范围,不变量就不算被保护。

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
`std::unique_ptr` 是 C++11 引入的一种智能指针,它提供了独占所有权的语义。通过调用 `std::move` 函数可以将 `std::unique_ptr` 对象的所有权转移到另一个 `std::unique_ptr` 对象中。 下面是一个示例,展示了如何使用 `std::move` 函数来转移 `std::unique_ptr` 对象的所有权: ```cpp #include <iostream> #include <memory> int main() { std::unique_ptr<int> sourcePtr = std::make_unique<int>(42); // 使用 std::move 转移 sourcePtr 对象的所有权到 targetPtr std::unique_ptr<int> targetPtr = std::move(sourcePtr); // sourcePtr 不再拥有有效的对象 if (sourcePtr) { std::cout << "sourcePtr 不为空指针" << std::endl; } else { std::cout << "sourcePtr 为空指针" << std::endl; } // targetPtr 拥有有效的对象 if (targetPtr) { std::cout << "targetPtr 不为空指针" << std::endl; std::cout << "targetPtr 的值: " << *targetPtr << std::endl; } else { std::cout << "targetPtr 为空指针" << std::endl; } return 0; } ``` 在上述示例中,我们首先创建了一个 `std::unique_ptr` 对象 `sourcePtr`,并使用 `std::make_unique` 方法分配了一个整数对象并将其赋值为42。接下来,我们使用 `std::move` 函数将 `sourcePtr` 对象的所有权转移到 `targetPtr`。然后,我们检查 `sourcePtr` 和 `targetPtr` 是否为空指针,并打印相应的信息。 输出结果将是: ``` sourcePtr 为空指针 targetPtr 不为空指针 targetPtr 的值: 42 ``` 请注意,在调用 `std::move` 后,源 `std::unique_ptr` 对象将不再拥有有效的对象,而目标 `std::unique_ptr` 对象将拥有源对象之前所指向的对象。 希望这个示例能对你有帮助!如果你还有其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值