C++ STL标准库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内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++ STL标准库std::vector的使用详解

    目录 1.简介 2.使用示例 3.构造.析构.赋值 3.1std::vector::vector构造函数 3.2std::vector::~vector析构函数 3.3std::vector::operator=“=”符号 4.Iterators迭代器 4.1std::vector::begin 4.2std::vector::end 4.3std::vector::rbegin 4.4std::vector::rend 4.5std::vector::cbegin(C++11) 4.6std:

  • C++入门笔记之std::vector容器详解

    目录 前言 1. vector的构造函数原型: 2. vector的赋值函数原型: 3. vector的容量和大小函数原型: 4. vector的插入和删除函数原型: 5. vector的存取操作函数原型: 6. vector的呼唤容器函数原型: 总结 前言 vector实质是C++的一个类,与数组很相似,但是vector的优势是可以动态扩展,不需要考虑其内存大小. 定义: 向量(Vector)是一个封装了动态大小数组的顺序容器(Sequence Container).跟任意其它类型容器一样,它

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

    目录 引子 查找原因 解决方法 结论 引子 但是笔者却发现了一个奇怪的现象,std::vector扩容时,对其中的元素竟然进行的是深复制.请看示例代码: #include <iostream> #include <vector> struct Test { Test() {std::cout << "Test" << std::endl;} ~Test() {std::cout << "~Test" <

  • C/C++标准库之转换UTC时间到local本地时间详解

    前言 UTC 时间DateTime.UtcNow 和 系统本地时间 DateTime.Now 相差8个时区 ,美国本地时间和北京时间相差15个时区: 美国,而一般使用UTC时间方便统一各地区时间差异. 场景 1.如果有面向全球用户的网站, 一般在存储时间数据时存储的是UTC格式的时间, 这样时间是统一的, 并可以根据当地时区来进行准确的转换. 2.存储本地时间的问题就在于如果换了时区, 那么显示的时间并不正确. 所以我们存储时间时最好还是存储UTC时间,便于正确的转换. 说明 1.C/C++标准

  • PHP写API输出的时用echo的原因详解

    写php API写的很少,最近才开始接口的写法,在框架里面一直用return,但是在api中retrun就失效了,为什么呢? 网友给出的答案: 1.return 一般用于函数或方法的返回. echo 才是输出(也就是显示) 如果你不输出,api有何作用. 2.return只能在php内部使用 对外,你要给别人解析,你就理解成输出,用echo 3.当然是echo了,如果是json最好加上header标识 <?php $ret = array( 'name' => 'fdipzone' ); he

  • C++顺序容器(vector、deque、list)的使用详解

    目录 一:STL(Standard Template Library),即标准模板库,是一个高效的C++程序库 二:STL组件 三:容器 四:类型成员 五:迭代器 六:顺序容器 七:顺序容器--向量(vector) 八:顺序容器--双端队列--deque 九:顺序容器 --列表--list 一:STL(Standard Template Library),即标准模板库,是一个高效的C++程序库 1.从实现层次看,整个STL是以一种类型参数化(type parameterized)的方式实现的,基

  • numpy库与pandas库axis=0,axis= 1轴的用法详解

    对数据进行操作时,经常需要在横轴方向或者数轴方向对数据进行操作,这时需要设定参数axis的值: axis = 0 代表对横轴操作,也就是第0轴: axis = 1 代表对纵轴操作,也就是第1轴: numpy库中横轴.纵轴 axis 参数实例详解: In [1]: import numpy as np #生成一个3行4列的数组 In [2]: a = np.arange(12).reshape(3,4) In [3]: a Out[3]: array([[ 0, 1, 2, 3], [ 4, 5,

  • python3中datetime库,time库以及pandas中的时间函数区别与详解

    1介绍datetime库之前 我们先比较下time库和datetime库的区别 先说下time 在 Python 文档里,time是归类在Generic Operating System Services中,换句话说, 它提供的功能是更加接近于操作系统层面的.通读文档可知,time 模块是围绕着 Unix Timestamp 进行的. 该模块主要包括一个类 struct_time,另外其他几个函数及相关常量. 需要注意的是在该模块中的大多数函数是调用了所在平台C library的同名函数, 所以

  • Java异常处理运行时异常(RuntimeException)详解及实例

      Java异常处理运行时异常(RuntimeException)详解及实例 RuntimeException RunntimeException的子类: ClassCastException 多态中,可以使用Instanceof 判断,进行规避 ArithmeticException 进行if判断,如果除数为0,进行return NullPointerException 进行if判断,是否为null ArrayIndexOutOfBoundsException 使用数组length属性,避免越

  • ASP.NET MVC中使用jQuery时的浏览器缓存问题详解

    介绍 尽管jQuery在浏览器ajax调用的时候对缓存提供了很好的支持,还是有必要了解一下如何高效地使用http协议. 首先要做的事情是在服务器端支持HTTP GET,定义不同的URL输出不同的数据(MVC里对应的就是action).如果要使用同一个地址获取不同的数据,那就不对了,一个HTTP POST也不行因为POST不能被缓存.许多开发人员使用POST主要有2个原因:明确了数据不能被缓存,或者是避免JSON攻击(JSON返回数组的时候可以被入侵). 缓存解释 jQuery全局对象里的ajax

  • Spring 整合 Hibernate 时启用二级缓存实例详解

    Spring 整合 Hibernate 时启用二级缓存实例详解 写在前面: 1. 本例使用 Hibernate3 + Spring3: 2. 本例的查询使用了 HibernateTemplate: 1. 导入 ehcache-x.x.x.jar 包: 2. 在 applicationContext.xml 文件中找到 sessionFactory 相应的配置信息并在设置 hibernateProperties 中添加如下代码: <!-- 配置使用查询缓存 --> <prop key=&q

随机推荐