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

本文探讨了C++中移动语义的使用及其与异常安全的关系,特别是在std::vector::push_back函数中如何选择使用移动构造还是拷贝构造以确保强异常安全性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

问题描述

 三种异常保证

附录


问题描述

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.超出了这个范围,不变量就不算被保护。

<think>嗯,用户问的是在C++程序设计中,哪些场景使用std::move比较多。首先,我需要回忆一下std::move的基本作用。std::move主要用于将对象转换为右值引用,这样可以触发移动语义,避免不必要的拷贝,提高性能。接下来,我得想想常见的应用场景有哪些。 首先想到的是在转移资源所有权的时候,比如智能指针或者自定义的资源管理类。比如unique_ptr,当需要转移所有权时,必须使用move,因为unique_ptr不能拷贝,只能移动。这时候std::move就派上用场了。 然后是容器操作,比如vectorpush_back或者emplace_back。当向容器中添加临时对象时,使用std::move可以将对象移动进去,而不是拷贝,特别是对于大型对象,这样可以提高效率。比如有一个大的字符串,要放进vector里,用move就能避免复制整个字符串的开销。 另外,自定义的移动构造函数和移动赋值运算符中,参数通常是一个右值引用,这时候如果需要将成员变量移动过来,就需要使用std::move。比如一个类里有字符串成员,移动构造函数里应该用std::move来转移这个字符串的所有权,而不是拷贝。 还有返回值优化(RVO)的情况下,虽然编译器会自动优化,但有时候显式使用std::move可以确保移动发生,尤其是在返回局部对象的时候,尤其是当返回类型和函数声明类型不完全匹配时,比如返回基类类型,而实际是派生类,这时候可能需要用move来触发移动构造。 另外,在函数参数传递的时候,如果函数接受右值引用参数,并且需要将参数传递给另一个函数,这时候可能需要用std::forward来完美转发,但如果是明确要转移所有权,也可以使用std::move。不过这里可能需要注意,如果参数是右值引用,使用std::move是对的,但如果是通用引用,可能需要用forward。 还有线程或异步任务中,当需要将资源转移到另一个线程时,比如传递一个unique_ptr到线程中,这时候必须用move,因为线程的构造函数接受右值引用。 不过要注意的是,使用std::move之后,原来的对象处于有效但未定义的状态,所以不能假设它还有原来的值,只能进行析构或重新赋值。比如一个被move过的string可能变成空字符串,但具体实现可能不同。 可能还需要考虑一些注意事项,比如当对象没有移动构造函数时,即使使用std::move,也会回退到拷贝构造,所以这种情况下用move可能不会有性能提升,反而可能让代码意图不清晰。 总结一下,常见的场景包括:转移智能指针所有权、容器操作中的元素添加、实现移动构造函数/赋值运算符、优化返回值、线程间传递资源等。不过需要确保对象支持移动语义,并且在使用后不再依赖原对象的状态。</think>在C++程序设计中,`std::move`主要用于触发移动语义以提升性能。以下是典型使用场景及说明: --- ### 1. **资源所有权转移** - **智能指针**:`unique_ptr`必须通过`std::move`转移所有权 ```cpp std::unique_ptr<int> ptr1 = std::make_unique<int>(42); std::unique_ptr<int> ptr2 = std::move(ptr1); // ptr1变为nullptr ``` - **自定义资源类**:文件句柄、网络连接等 ```cpp class Socket { int fd_; public: Socket(Socket&& other) : fd_(other.fd_) { other.fd_ = -1; // 标记原对象为无效状态 } }; ``` --- ### 2. **容器高效操作** - **添加元素**:避免大对象拷贝 ```cpp std::vector<std::string> vec; std::string large_str(1024, 'a'); vec.push_back(std::move(large_str)); // 移动而非拷贝 ``` - **容器间转移**:批量移动元素 ```cpp std::vector<std::string> source = {"a", "b", "c"}; std::vector<std::string> target = std::move(source); // source清空 ``` --- ### 3. **实现移动语义** - **移动构造函数**: ```cpp class MyClass { std::string data_; public: MyClass(MyClass&& other) : data_(std::move(other.data_)) {} }; ``` - **移动赋值运算符**: ```cpp MyClass& operator=(MyClass&& other) { data_ = std::move(other.data_); return *this; } ``` --- ### 4. **返回值优化(RVO)补充** - **强制移动返回值**: ```cpp std::vector<int> create_data() { std::vector<int> tmp(1000); return std::move(tmp); // 显式触发移动(编译器可能自动优化) } ``` --- ### 5. **线程/异步任务** - **转移资源到线程**: ```cpp auto task = [](std::unique_ptr<Data> data) { /* ... */ }; std::unique_ptr<Data> data = std::make_unique<Data>(); std::thread worker(task, std::move(data)); // 必须移动 ``` --- ### ⚠️ 注意事项 1. **对象有效性**:被移动后的对象处于有效但未定义状态 ```cpp std::string s1 = "hello"; std::string s2 = std::move(s1); // s1可能为空,但可安全析构或重新赋值 ``` 2. **避免误用**:不要移动临时对象 ```cpp // 错误!临时对象本身是右值 vec.push_back(std::move(std::string("tmp"))); ``` 3. **回退机制**:若无移动构造函数,仍会执行拷贝操作 --- ### 最佳实践 - 优先用于管理大型资源/不可拷贝对象 - 在明确需要转移所有权的场景使用 - 配合`noexcept`声明移动操作(确保异常安全) 通过合理使用`std::move`,可显著减少不必要的拷贝操作,提升程序性能,尤其适用于资源密集型场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值