C++ STL标准库std::vector扩容时进行深复制原因详解

 更新时间:2022年08月15日 14:42:57   作者:fl2011sx  
我们知道,std::vector之所以可以动态扩容,同时还可以保持顺序存储,主要取决于其扩容复制的机制。当容量满时,会重新划分一片更大的内存区域,然后将所有的元素拷贝过去

引子

但是笔者却发现了一个奇怪的现象,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内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 详解C++的JSON静态链接库JsonCpp的使用方法

    详解C++的JSON静态链接库JsonCpp的使用方法

    这篇文章主要介绍了C++的JSON静态链接库JsonCpp的使用方法,演示了使用JsonCpp生成和解析JSON的方法,以及C++通过JSON方式的socket通信示例,需要的朋友可以参考下
    2016-03-03
  • C++哈希应用之位图,哈希切分与布隆过滤器详解

    C++哈希应用之位图,哈希切分与布隆过滤器详解

    这篇文章主要为大家详细介绍了C++哈希应用中的位图、哈希切分与布隆过滤器,文中的示例代码讲解详细,具有一定的学习价值,需要的可以参考一下
    2023-04-04
  • C语言数据结构之队列的定义与实现

    C语言数据结构之队列的定义与实现

    队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(head)进行删除操作,而在表的后端(tail)进行插入操作。本文将详细讲讲C语言中队列的定义与实现,感兴趣的可以了解一下
    2022-07-07
  • C语言实现简单的<三子棋>案例

    C语言实现简单的<三子棋>案例

    这篇文章主要介绍了C语言实现简单的《三子棋》,本文通过功能区分一步步实现该案例,通过逐步的解析和代码列举,以下就是详细内容,需要的朋友可以参考下
    2021-07-07
  • 文件编译时出现multiple definition of ''xxxxxx''的具体解决方法

    文件编译时出现multiple definition of ''xxxxxx''的具体解决方法

    以下是对文件编译时出现multiple definition of 'xxxxxx'的解决方法进行了详细的分析介绍,如也遇到此问题的朋友们可以过来参考下
    2013-07-07
  • 浅谈c语言中转义字符的用法及注意事项

    浅谈c语言中转义字符的用法及注意事项

    下面小编就为大家带来一篇浅谈c语言中转义字符的用法及注意事项。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-08-08
  • C++ stringstream类用法详解

    C++ stringstream类用法详解

    这篇文章主要介绍了C++ stringstream类用法详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-08-08
  • C++分析类的对象作类成员调用构造与析构函数及静态成员

    C++分析类的对象作类成员调用构造与析构函数及静态成员

    终于到了对象的初始化和清理的最后阶段了,在这里分享一个cpp里有多个类时,一个类的对象作为另一个类成员的时候构造函数和析构函数调用的时机。还有一个静态成员也是经常考到的点,在这篇博客将会详解其概念并举出案例巩固,让我们开始
    2022-05-05
  • C++之boost::array的用法

    C++之boost::array的用法

    这篇文章主要介绍了C++之boost::array的用法,以实例的形式简单讲述了静态数组的容器boost::array的使用技巧,具有一定的参考借鉴价值,需要的朋友可以参考下
    2014-10-10
  • C语言数据结构实现链表逆序并输出

    C语言数据结构实现链表逆序并输出

    这篇文章主要介绍了C语言数据结构实现链表逆序并输出的相关资料,需要的朋友可以参考下
    2017-04-04

最新评论