如何确保从另一个容器创建 std::vector 时一次性内存分配?

How to ensure one time memory allocation while creation std::vector from another container?

提问人:Damir Tenishev 提问时间:11/13/2023 更新时间:11/13/2023 访问量:72

问:

当我需要从另一个容器(假设另一个向量)的某些元素创建 std::vector 时,确保新向量仅在内存中分配一次(技术上是两次,但让我们忽略起始 tine size)的最安全方法是:

std::vector<int> v1 = { 1, 2, 3, 4, 5, 6 };
const size_t amount = 3;
std::vector<int> v2;
v2.reserve(amount);
v2.insert(v2.begin(), v1.begin(), v1.begin() + amount);

同时,这段代码比非常简单的构造要长得多,从向量中提取子向量的最佳方法,如

std::vector<int> v1 = { 1, 2, 3, 4, 5, 6 };
const size_t amount = 3;
std::vector<int> v2(v1.begin(), v1.begin() + amount);

std::vector<int> v1 = { 1, 2, 3, 4, 5, 6 };
const size_t amount = 3;
std::vector<int> v2 = {v1.begin(), v1.begin() + amount};

问题是,后一种情况是否确保 v2 的内存只分配一次,或者特定的实现可以很容易地使用诸如 push_back 或 emplace_back 循环之类的东西(我夸大其词)并导致许多容器内存重新分配在其创建过程中作为副本?

是标准保证的,或者至少我可以依赖他们不会多次重新分配内存的现有实现吗?

我很担心,因为我使用巨大的容器,并希望确保这些更具可读性的代码不会对性能产生影响。

C++ STL

评论

2赞 Ted Lyngmo 11/13/2023
“后一种情况是否确保 v2 的内存只会分配一次”——我不确定标准是怎么说的,但如果它们在内部没有随机访问迭代器,这似乎很奇怪。不过,如果你从中填充它,那就更难了。然后我会像你一样做,先手动保留,然后或.reservestd::listinsertassign

答:

2赞 Artyer 11/13/2023 #1

根据 https://en.cppreference.com/w/cpp/container/vector/vector#Complexity,列出的第 5 个构造函数(对于两个迭代器),对于非输入迭代器,不会发生重新分配。构造函数必须执行等效于符合标准的工作。reserve(distance(first, last))

因此,您将只有一个分配。std::vector<int> v2(v1.begin(), v1.begin() + amount);

评论

0赞 Damir Tenishev 11/13/2023
谢谢。那么,这也包括在内吗?(我接受了 Ted 的回答,只是因为提到了标准,尽管它们绝对平等且有用(两者都标记);对不起,这个选择)。std::vector<int> v2 = {v1.begin(), v1.begin() + amount};
4赞 Ted Lyngmo 11/13/2023 #2

我在 C++23 标准中找到了这段话:

24.3.11.2 构造函数 [vector.cons]

template<class InputIterator>
constexpr vector(InputIterator first, InputIterator last,
const Allocator& = Allocator());
  1. 效果:使用指定的分配器构造等于范围 的 。vector[first, last)
  2. 复杂性:仅对 T 的复制构造函数进行 N 次调用(其中 N 是第一个和最后一个之间的距离),如果第一个和最后一个迭代器是正向、双向或随机访问类别,则不进行重新分配。如果它们只是输入迭代器,它会对 T 的复制构造函数进行顺序 N 调用,并对日志 N 进行重新分配。

这意味着符合要求的实现将至少与手动调用 + 一样有效。reserveinsert

评论

0赞 Damir Tenishev 11/13/2023
谢谢。那么,如果我从具有 LegacyBidirectionalIterator 的 std::list 复制,这个迭代器是否被视为“双向”类别或 LegacyIterator,它不适合这里并且会发生重新分配?
0赞 Ted Lyngmo 11/13/2023
@DamirTenishev 不客气!我假设重新分配可能会发生,因为不能在恒定时间内执行,但标准另有规定。我想这意味着如果给定非随机访问正向/双向迭代器,它必须运行整个范围才能首先计算元素。std::list::iteratorlast - first
0赞 Damir Tenishev 11/13/2023
明白了。问题更多的是关于标准中的术语;也就是说,LegacyBidirectionalIterator 是否可以在上面的上下文中被视为“双向”类别,或者某个地方是标准的,明确指出这些(旧和新)层次结构是不同的,具有不同的行为和要求,不能这样考虑。至于通过列表进行计数,我想在最合理的情况下,这种计数(如果有必要并且数量没有被缓存)将比重新分配(对于实时系统和内存受限的控制器)便宜得多。
0赞 Ted Lyngmo 11/13/2023
@DamirTenishev 是的,在此上下文中,LegacyBidirectionalIterator 算作双向。该标准没有提到使用什么技术,只提到复杂性要求。我同意,对于这些迭代器来说,先计数可能是合理的。我只是没想到除了 RandomAccessIterators 之外的任何东西都有这样的要求:-)