C++ std::vector
fl2011sx 人气:0引子
但是笔者却发现了一个奇怪的现象,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
来声明。
加载全部内容