【STL容器vector零拷贝】基于模板和移动语义的std::vector零拷贝添加元素设计

前言

  • nav2系列教材,yolov11部署,系统迁移教程我会放到年后一起更新,最近年末手头事情多,还请大家多多谅解。
#include <iostream>
#include <vector>
class MyClass
{
    public:
    MyClass()
    {
        std::cout<<"MyClass()"<<std::endl;
    }
    MyClass(const MyClass& other)
    {
        std::cout<<"MyClass(const MyClass& other)"<<std::endl;
    }
    MyClass(MyClass&& other) noexcept {
        std::cout << "MyClass(MyClass&& other)" << std::endl;
    }
};
int main()
{
    MyClass m1,m2,m3;
    std::vector<MyClass> vec={m1,m2,m3};
    return 0;
}
  • 揭晓答案:

  • 请添加图片描述

  • 怎么样?是否出乎你的意料?MyClass在短短一行代码里头被拷贝了6次!!!我们看看这个是怎么回事。

  • 上述情况也是为了应付你无法直接调用emplace直接进行类内构造

  • 老规矩先上全部代码

template <typename T, typename... Args>
void vector_add_with_reserve(std::vector<T>& vec, Args&... elements) {
    vec.reserve(vec.size() + sizeof...(elements));  
    (vec.push_back(std::move(elements)), ...);    
}

1 尝试引入移动语义

  • 不慌张,结合上节所学的移动语义,你可能想到这样解决:
MyClass m1,m2,m3;
std::vector<MyClass> vec = {std::move(m1), std::move(m2), std::move(m3)}; // 使用 std::move 转移对象
  • 我们来看看运行结果

  • 请添加图片描述

  • 可以发现,拷贝的次数下降了,但是仍然存在。


2 vector创建元素分析

2-1 知识回顾
  1. std::vector 的内存管理和扩容:
    • std::vector 在插入元素时,可能会遇到内存重新分配的问题。一般来说,当 std::vector 的元素数量超过当前分配的容量时,它会扩展其存储空间。这通常会触发如下步骤:
      • 内存分配:当 std::vector 扩展时,会分配一块新的内存区域,通常是当前容量的两倍。
      • 元素拷贝或移动:在内存扩展后,原先的元素必须移动到新的内存区域。如果元素类型支持移动构造,通常会调用移动构造函数;如果不支持,则会调用拷贝构造函数。
  2. std::vector::insertstd::vector::emplace
    • 这两个函数都是用于向 std::vector 中插入元素。在插入过程中,如果 std::vector 需要扩展容量,通常会发生以下操作:
      • std::vector::insertstd::vector::push_back 会向 vector 末尾添加元素。如果此时 vector 已满,它会重新分配内存。
      • 在重新分配内存时,元素会被依次复制或移动到新的内存区域中。
  3. std::vector::resizestd::vector::reserve**:
    • resize:该方法会改变容器的大小,必要时分配新内存。如果容器增加了元素,并且新分配的内存不足以容纳它们,它会重新分配并拷贝或移动元素。
    • reserve:==该方法预先分配足够的内存,而不改变容器的大小。==如果容器已经有足够的内存,调用 reserve 不会触发重新分配。这是避免频繁重新分配和拷贝的好方法。
2-2 为何拷贝和移动交替发生
  1. 预分配容量不足
    如果在初始化 std::vector 时没有提前 reserve 足够的空间,std::vector 会根据需要重新分配内存。在重新分配时,std::vector 会将现有元素搬移到新的内存位置。如果 std::move 使用正确,它应该调用移动构造函数,但如果在某些实现中(例如容器大小调整时),编译器没有完全优化,也可能会进行拷贝构造。
  2. std::vector 内存分配和优化
    GCC 的实现中,std::vector 在扩容时会分配一个新的内存块,并将元素从旧内存区域迁移到新区域。在这过程中:
    • 如果 std::move 被正确应用且元素可以被移动,那么将调用移动构造函数。
    • 如果由于某些原因容器的容量重新分配时发生了不必要的元素拷贝(例如编译器没有足够优化,或者元素类型不支持高效的移动),则会调用拷贝构造函数。
  3. std::vector 的容器扩展过程
    在容器扩展时,std::vector 会将旧元素移动到新内存中。这通常会触发移动构造函数,但如果编译器没有优化好内存管理,可能会发生不必要的拷贝。比如,如果容器在分配新内存后没有直接采用新内存而是继续使用旧内存,可能会触发一些额外的拷贝操作。

3 修正方案:

3-1 直接修正
  • 我们可以这样解决
std::vector<MyClass> vec;
vec.reserve(3);  // 提前为 3 个元素分配空间
vec.push_back(std::move(m1));
vec.push_back(std::move(m2));
vec.push_back(std::move(m3));
  • 请添加图片描述

  • 可以看到拷贝被避免了。


3-2 封装起来
  • 我们也可以封装起来
template <typename T, typename... Args>
void vector_add_with_reserve(std::vector<T>& vec, Args&... elements) {
    vec.reserve(vec.size() + sizeof...(elements));  
    (vec.push_back(std::move(elements)), ...);    
}
// 使用
vector_add_with_reserve(vec, m1,m2,m3);
  • 我们来看每一部分对应的细节:
  1. 模板参数 TArgs...
template <typename T, typename... Args>
  • T:这是 std::vector<T> 中的元素类型。可以理解为 vector 中元素的类型。
  • Args...:这是一个 参数包(parameter pack),表示一个不定数量的类型。你可以传递任意数量的参数给这个函数,每个参数的类型都可以不同。
  1. 函数签名
void vector_add_with_reserve(std::vector<T>& vec, Args&... elements)
  • std::vector<T>& vec:这是一个对 std::vector<T> 的引用,用于接收你要添加元素的容器。
  • Args&... elements:这是参数包,表示你传递给函数的多个参数。每个参数都是右值引用的引用类型 Args&
  1. 扩容
vec.reserve(vec.size() + sizeof...(elements));
  • vec.size():表示 vec 当前的大小(即当前 vector 中元素的数量)。
  • sizeof...(elements):这是一个 参数包的大小(即 elements 包含的参数数量)。例如,如果你传递了 3 个元素,它的值将是 3。
  • reserve 用于预留 std::vector 中的空间。在调用 reserve 时,我们将 vec.size()sizeof...(elements) 相加,确保在插入新元素时有足够的空间。这样做是为了避免在 push_back 时反复进行内存重分配。
  1. 加入元素
(vec.push_back(std::move(elements)), ...);
  • 这是一个 折叠表达式(fold expression),是 C++17 引入的语法。
  • 折叠表达式会展开并逐个处理 elements 参数包中的每个元素。
  • 具体地说,这里 (vec.push_back(std::move(elements)), ...); 展开后,类似于以下代码:
vec.push_back(std::move(elements1)); 
vec.push_back(std::move(elements2)); 
vec.push_back(std::move(elements3)); // ...
  • 运行

  • 请添加图片描述

  • 可以发现拷贝也被防止了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值