C++ STL标准库std::vector扩容时进行深复制原因详解
作者:fl2011sx
引子
但是笔者却发现了一个奇怪的现象,std::vector
扩容时,对其中的元素竟然进行的是深复制。请看示例代码:
#include <iostream> #include <vector> struct Test { Test() {std::cout << "Test" << std::endl;} ~Test() {std::cout << "~Test" << std::endl;} Test(const Test &) {std::cout << "Test copy" << std::endl;} Test(Test &&) {std::cout << "Test move" << std::endl;} }; int main(int argc, const char *argv[]) { std::vector<Test> ve; ve.emplace_back(); ve.emplace_back(); ve.emplace_back(); return 0; }
打印结果如下:
Test
Test
Test copy
~Test
Test
Test copy
Test copy
~Test
~Test
~Test
~Test
~Test
由于我们没有调用reverse
函数,所以默认只分配了一个元素的大小。第一次emplace_back
时,仅进行了一次普通构造。第二次emplace_back
时,就需要进行扩容,然后把第一个元素拷贝过去,再释放原来的对象。所以这里除了有一次新的构造以外,还有一次复制和释放。后面的行为类似,不再赘述,
但关键问题就在于,Test
类明明实现了移动构造(浅复制),可这里竟然调用了拷贝构造(深复制)。
如果vector
扩容无脑调用拷贝构造,那么这个对象如果含有很多外链的成员(比如说指向buffer的指针、指向其他对象的指针等),调用拷贝构造就意味着要把这些链接的对象全部都重新构造一遍。这对于vector
自身扩容来说,显然是没有必要的,会极度浪费内存空间。
查找原因
基于上述理由,我认为STL的开发者不可能连这个问题都考虑不到,但想不通为什么我明明实现了移动构造,却不能调用。
带着这样的疑问我去研读了STL的源码(GNU版本),在vector
扩容时,会调用_M_realloc_insert
函数,该函数在vector.tcc文件中实现。在这个函数里面对已有元素进行拷贝的时候,看到了类似这样的代码:
__new_finish = std::__uninitialized_move_if_noexcept_a (__old_start, __position.base(), __new_start, _M_get_Tp_allocator()); ++__new_finish;
有趣的就是这个__uninitialized_move_if_noexcept_a
,我们找到这个函数的实现:
template<typename _InputIterator, typename _ForwardIterator, typename _Allocator> inline _ForwardIterator __uninitialized_move_if_noexcept_a(_InputIterator __first, _InputIterator __last, _ForwardIterator __result, _Allocator& __alloc) { return std::__uninitialized_copy_a (_GLIBCXX_MAKE_MOVE_IF_NOEXCEPT_ITERATOR(__first), _GLIBCXX_MAKE_MOVE_IF_NOEXCEPT_ITERATOR(__last), __result, __alloc); }
再看一下_GLIBCXX_MAKE_MOVE_IF_NOEXCEPT_ITERATOR
的实现
#if __cplusplus >= 201103L #define _GLIBCXX_MAKE_MOVE_IF_NOEXCEPT_ITERATOR(_Iter) std::__make_move_if_noexcept_iterator(_Iter) #else #define _GLIBCXX_MAKE_MOVE_IF_NOEXCEPT_ITERATOR(_Iter) (_Iter) #endif // C++11
也就是说,在C++11以前,这玩意就是对象本身(毕竟C++11以前还没有移动构造),而在C++11以后被定义成了__make_move_if_noexcept_iterator
,继续查看其定义。
template<typename _Iterator, typename _ReturnType = typename conditional<__move_if_noexcept_cond <typename iterator_traits<_Iterator>::value_type>::value, _Iterator, move_iterator<_Iterator>>::type> inline _GLIBCXX17_CONSTEXPR _ReturnType __make_move_if_noexcept_iterator(_Iterator __i) { return _ReturnType(__i); }
这里用了一个conditional
,来判断这个迭代器的类型,如果__move_if_noexcept_cond
为真,就取迭代器本身,否则就取移动迭代器。看起来问题就在这里了,之前我们的例程中的Test一定就是符合了这个__move_if_noexcept_cond
,导致用了原始迭代器。
继续深挖这个__move_if_noexcept_cond
,看到这样的代码:
template<typename _Tp> struct __move_if_noexcept_cond : public __and_<__not_<is_nothrow_move_constructible<_Tp>>, is_copy_constructible<_Tp>>::type { };
也就是说,如果一个类,不存在不会抛出异常的移动构造函数并且可拷贝,那么就为真。
Test类显然符合,所以vector<Test>
在复制时用了普通的迭代器进行了遍历,自然就会调用拷贝构造函数进行复制了。
解决方法
所以,我们需要让Test
不符合__move_if_noexcept_cond
的条件,也就是这里要将移动构造函数声明为noexcept
表示它不会抛出异常,这样vector<Test>
在复制时就会使用移动迭代器(就是会包装一层std::move
),从而触发移动构造。
顺道我们也看一眼移动迭代器的原理:
template<typename _Iterator> class move_iterator { _Iterator _M_current; // ... public: using iterator_type = _Iterator; explicit _GLIBCXX17_CONSTEXPR move_iterator(iterator_type __i) : _M_current(std::move(__i)) { } // ... }
确实调用了std::move
,证明我们的思路没错。
所以,修改Test
代码,实现noexcept
移动构造:
struct Test { long a, b, c, d; Test() {std::cout << "Test" << std::endl;} ~Test() {std::cout << "~Test" << std::endl;} Test(const Test &) {std::cout << "Test copy" << std::endl;} Test(Test &&) noexcept {std::cout << "Test move" << std::endl;} }; int main(int argc, const char *argv[]) { std::vector<Test> ve; ve.emplace_back(); ve.emplace_back(); ve.emplace_back(); return 0; }
打印结果如下:
Test
Test
Test move
~Test
Test
Test move
Test move
~Test
~Test
~Test
~Test
~Test
这次如我们所愿,调用了移动构造。
结论
STL中考虑到异常的情况,因此,像这种容器内部的复制行为,是要求不能够发生异常的,因此,只有当移动构造函数声明为noexcept
的时候才会调用,否则将统一调用拷贝构造函数。
然而,在移动构造函数中本来就不应该抛出异常,因此,在大多数情况下,移动构造函数都应该用noexcept
来声明。
到此这篇关于C++ STL标准库std::vector扩容时进行深复制原因详解的文章就介绍到这了,更多相关C++ std::vector内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!