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

本文探讨了C++中std::vector扩容时为何未使用移动构造函数的问题。通过分析源码,发现扩容过程中因异常安全性的考虑,只有当移动构造函数声明为noexcept时才会调用。解决方案是将移动构造函数声明为noexcept,以利用移动语义提高效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引子

我们知道,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来声明。

<think>好的,我现在需要回答用户关于C++std::vector<std::string>的用法和含义的问题。首先,我得确认用户的需求,他们可能刚接触C++,或者对STL容器不太熟悉,想了解这个特定容器的基本使用方法和应用场景。 首先,我应该解释std::vector<std::string>是什么。std::vectorC++标准模板库中的一个动态数组模板类,而std::string则是处理字符串的类。所以结合起来,这个类型就是一个可以动态调整大小的数组,每个元素都是字符串。需要强调它的优势,比如自动内存管理、动态扩展、随机访问等。 接下来,用户需要具体的用法。我应该分步骤说明,比如如何声明和初始化、添加元素、访问元素、遍历、删除元素以及常用方法。每个部分都要给出示例代码,这样用户更容易理解。比如初始化可以用默认构造、列表初始化或者从其他容器复制。添加元素可以用push_back或emplace_back,访问可以用[]运算符或at(),遍历可以用迭代器或范围循环。 然后,要考虑用户可能遇到的常见问题,比如性能问题(比如预分配空间)、异常安全(比如使用emplace_back减少拷贝)、线程安全等。这些注意事项能帮助用户避免常见陷阱。 另外,用户提供的引用中有涉及vector和map的结合使用,但用户的问题集中在vector<string>上。不过可能需要稍微提到与其他容器的嵌套使用,但重点还是保持在本主题上。引用中的例子可以作为实际应用的参考,比如在配置加载中使用vector<string>存储字符串列表,如引用3中的例子。 需要确保LaTeX格式正确,比如变量名用$...$包裹,独立公式用$$...$$,但这个问题中可能涉及代码较多,数学公式较少。不过如果有提到复杂度之类的,可以用LaTeX,比如O(1)的访问间。 最后,生成相关问题,帮助用户深入理解。可能的问题包括与其他容器的比较、底层实现、性能优化、实际应用案例等。 现在组织结构:先含义,再用法分点,注意事项,示例代码,相关问题。确保代码格式正确,用中文回答,引用相关文献。用户提供的引用1、2、3中的例子可以作为实际应用场景的例子,比如引用3中的vtTemp用来存储字符串,通过迭代器遍历输出。 注意检查是否有遗漏的关键点,比如vector的内存管理、迭代器失效的情况,但可能用户作为初学者不需要太深入,保持回答简洁实用即可。</think>### C++中`std::vector<std::string>`的用法及含义 #### 含义解析 `std::vector<std::string>`是C++标准模板库(STL)中的一种容器组合: 1. **`std::vector`**:动态数组,支持快速随机访问和动态扩容[^1]。 2. **`std::string`**:封装字符串操作的类,支持动态字符序列管理。 组合后,它表示一个存储字符串的动态数组,适用于需要灵活管理字符串集合的场景(如配置文件解析、文本处理)[^3]。 --- #### 核心用法分步说明 ##### 1. **声明与初始化** ```cpp #include <vector> #include <string> // 空向量 std::vector<std::string> vec1; // 初始化列表 std::vector<std::string> vec2 = {"apple", "banana", "cherry"}; // 指定初始大小和默认值 std::vector<std::string> vec3(5, "default"); // 5个"default"字符串 ``` ##### 2. **添加元素** ```cpp vec1.push_back("hello"); // 追加元素(可能触发拷贝) vec1.emplace_back("world"); // 原地构造(高效,减少拷贝)[^2] ``` ##### 3. **访问元素** ```cpp // 通过下标访问(不检查越界) std::string first = vec2[0]; // "apple" // 通过at()访问(越界抛出异常) std::string second = vec2.at(1); // "banana" // 获取首尾元素 std::string front = vec2.front(); std::string back = vec2.back(); ``` ##### 4. **遍历元素** ```cpp // 迭代器遍历 for (auto it = vec2.begin(); it != vec2.end(); ++it) { std::cout << *it << std::endl; } // 范围for循环(C++11+) for (const auto& s : vec2) { std::cout << s << std::endl; } ``` ##### 5. **删除元素** ```cpp vec2.pop_back(); // 删除最后一个元素 vec2.erase(vec2.begin() + 1); // 删除第二个元素("banana") vec2.clear(); // 清空所有元素 ``` ##### 6. **常用方法** ```cpp bool isEmpty = vec2.empty(); // 判断是否为空 size_t size = vec2.size(); // 当前元素数量 vec2.resize(10); // 调整容量为10 vec2.shrink_to_fit(); // 释放多余内存 ``` --- #### 注意事项 1. **性能优化**:预分配空间减少扩容开销 ```cpp vec1.reserve(100); // 预分配100个元素的内存 ``` 2. **迭代器失效**:插入/删除操作可能导致迭代器失效,需重新获取。 3. **线程安全**:多线程环境下需自行加锁保护。 --- #### 应用示例(参考引用[3]) ```cpp std::vector<std::string> vtTemp; GetConfigState(vtTemp); // 假设从配置加载数据 for (const auto& str : vtTemp) { std::cout << str << std::endl; } ``` ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

borehole打洞哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值