前言
nav2
系列教材,yolov11
部署,系统迁移教程我会放到年后一起更新,最近年末手头事情多,还请大家多多谅解。
- 上一期我们提到了
移动拷贝
,我们这一期来看一个关于std::vector
一个存储类对象时候拷贝元素的问题。 - 我们直接来看这样一段代码:无奖竞猜,猜猜下面代码的输出
#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创建元素分析
std::vector
源码位于 GCC 标准库实现中的stl_vector.h
,具体位置可以参考 libstdc+±v3/include/bits/stl_vector.h。
2-1 知识回顾
std::vector
的内存管理和扩容:std::vector
在插入元素时,可能会遇到内存重新分配的问题。一般来说,当std::vector
的元素数量超过当前分配的容量时,它会扩展其存储空间。这通常会触发如下步骤:- 内存分配:当
std::vector
扩展时,会分配一块新的内存区域,通常是当前容量的两倍。 - 元素拷贝或移动:在内存扩展后,原先的元素必须移动到新的内存区域。如果元素类型支持移动构造,通常会调用移动构造函数;如果不支持,则会调用拷贝构造函数。
- 内存分配:当
std::vector::insert
和std::vector::emplace
:- 这两个函数都是用于向
std::vector
中插入元素。在插入过程中,如果std::vector
需要扩展容量,通常会发生以下操作:std::vector::insert
或std::vector::push_back
会向vector
末尾添加元素。如果此时vector
已满,它会重新分配内存。- 在重新分配内存时,元素会被依次复制或移动到新的内存区域中。
- 这两个函数都是用于向
std::vector::resize
和std::vector::reserve
**:resize
:该方法会改变容器的大小,必要时分配新内存。如果容器增加了元素,并且新分配的内存不足以容纳它们,它会重新分配并拷贝或移动元素。reserve
:==该方法预先分配足够的内存,而不改变容器的大小。==如果容器已经有足够的内存,调用reserve
不会触发重新分配。这是避免频繁重新分配和拷贝的好方法。
2-2 为何拷贝和移动交替发生
- 预分配容量不足:
如果在初始化std::vector
时没有提前reserve
足够的空间,std::vector
会根据需要重新分配内存。在重新分配时,std::vector
会将现有元素搬移到新的内存位置。如果std::move
使用正确,它应该调用移动构造函数,但如果在某些实现中(例如容器大小调整时),编译器没有完全优化,也可能会进行拷贝构造。 std::vector
内存分配和优化:
GCC 的实现中,std::vector
在扩容时会分配一个新的内存块,并将元素从旧内存区域迁移到新区域。在这过程中:- 如果
std::move
被正确应用且元素可以被移动,那么将调用移动构造函数。 - 如果由于某些原因容器的容量重新分配时发生了不必要的元素拷贝(例如编译器没有足够优化,或者元素类型不支持高效的移动),则会调用拷贝构造函数。
- 如果
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);
- 我们来看每一部分对应的细节:
- 模板参数
T
和Args...
template <typename T, typename... Args>
T
:这是std::vector<T>
中的元素类型。可以理解为vector
中元素的类型。Args...
:这是一个 参数包(parameter pack),表示一个不定数量的类型。你可以传递任意数量的参数给这个函数,每个参数的类型都可以不同。
- 函数签名
void vector_add_with_reserve(std::vector<T>& vec, Args&... elements)
std::vector<T>& vec
:这是一个对std::vector<T>
的引用,用于接收你要添加元素的容器。Args&... elements
:这是参数包,表示你传递给函数的多个参数。每个参数都是右值引用的引用类型Args&
。
- 扩容
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
时反复进行内存重分配。
- 加入元素
(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)); // ...
-
运行
-
-
可以发现拷贝也被防止了。