std::vector扩容时为何进行深复制?

std::vector扩容时为何进行深复制?

引子

我们知道,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 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 _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 conditional<__move_if_noexcept_cond

::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

struct __move_if_noexcept_cond

: public __and_<__not_>,

is_copy_constructible<_Tp>>::type { };

也就是说,如果一个类,不存在不会抛出异常的移动构造函数并且可拷贝,那么就为真。

Test类显然符合,所以vector在复制时用了普通的迭代器进行了遍历,自然就会调用拷贝构造函数进行复制了。

解决方法

所以,我们需要让Test不符合__move_if_noexcept_cond的条件,也就是这里要将移动构造函数声明为noexcept表示它不会抛出异常,这样vector在复制时就会使用移动迭代器(就是会包装一层std::move),从而触发移动构造。

顺道我们也看一眼移动迭代器的原理:

template

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 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来声明。

上一篇: 定量探究强法与强克的换算关系
下一篇: 家用车一般开多少年再换

相关文章

丝绸之家(SILK MAISON) 女士西服
互联网行业10大黄金岗位揭秘:副业转型必看的职业指南
艾伦往事家具,重温经典,打造现代家居新风尚
燕王扫北,一个让河北人代代相传的基因记忆
世预赛:瑞典VS瑞士 今日内附推荐比分
2023年世界盃籃球賽