我们知道,std::vector
之所以可以动态扩容,同时还可以保持顺序存储,主要取决于其扩容复制的机制。当容量满时,会重新划分一片更大的内存区域,然后将所有的元素拷贝过去。
但是笔者却发现了一个奇怪的现象,std::vector
扩容时,对其中的元素竟然进行的是深复制。请看示例代码:
#include
#include
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
不符合__move_if_noexcept_cond
的条件,也就是这里要将移动构造函数声明为noexcept
表示它不会抛出异常,这样vector
在复制时就会使用移动迭代器(就是会包装一层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
来声明。