【问题标题】:Does a vector assignment invalidate the `reserve`?向量分配是否会使“保留”无效?
【发布时间】:2014-06-17 09:52:07
【问题描述】:

假设我写

std::vector<T> littleVector(1);
std::vector<T> bigVector;

bigVector.reserve(100);
bigVector = littleVector;

标准是否规定bigVector 仍将保留 100 个元素?或者如果我要使用push_back 99 个元素,我会经历内存重新分配吗?也许它甚至因 STL 实现而异。

这在之前讨论过here,但没有给出标准参考。

【问题讨论】:

  • @davidhigh:是的,它会回答我的特定编译器和 STL 实现的问题,但我也可以用类似的方式回答“i = i++ 做了什么”之类的问题。
  • 非常好的问题,+1。我在标准中找不到任何直截了当的内容。不过,分配器要求的黑暗魔法可能会带来一些东西。
  • 在 MSVC 中,它似乎保持正确的保留,除非 littleVector 有超过 100 个对象。
  • 我的编译器 (msvc12) 将容量保持在 100。
  • g++ 实现明确记录了容量是复制的;此外,标准明确要求assign 等同于erase,后跟insert(不受源capacity 的影响)。 OTOH,我发现标准中没有规定operator= 的行为方式与assign 相同。

标签: c++ language-lawyer


【解决方案1】:

不幸的是,该标准没有详细说明分配器感知序列容器分配的行为,严格来说确实是不一致的。

我们知道(从表 28 和 23.2.1p7)如果 allocator_traits&lt;allocator_type&gt;::propagate_on_container_copy_assignment::valuetrue,那么分配器在复制分配时被替换。此外,从表 96 和表 99 中我们发现,复制分配的复杂性线性,操作a = t后置条件a == t,即(表 96)distance(a.begin(), a.end()) == distance(t.begin(), t.end()) &amp;&amp; equal(a.begin(), a.end(), t.begin())。从 23.2.1p7 开始,复制赋值后,如果分配器传播,则a.get_allocator() == t.get_allocator()

关于向量容量,23.3.6.3 [vector.capacity]有:

5 - 备注: 重新分配使所有引用序列中元素的引用、指针和迭代器无效。保证在调用 reserve() 之后发生的插入过程中不会发生重新分配,直到插入会使向量的大小大于 capacity() 的值。

如果我们以library DR341 作为阅读标准的指南:

但是,23.3.6.3 [vector.capacity] 第 5 段的措辞阻止了在调用 reserve() 之后减少向量的容量。这使习语无效,因为这样可以防止 swap() 减少容量。 [...]

通过在 23.3.6.3 中添加段落解决了 DR341:

void swap(vector&lt;T,Allocator&gt;&amp; x);
7 - 效果:*this的内容和capacity()x的内容交换。
8 - 复杂性: 恒定时间。

结论是,从图书馆委员会的角度来看,如果在 23.3.6.3 中提及,操作只会修改 capacity()。 23.3.6.3 下未提及复制分配,因此不会修改capacity()。 (移动分配也有同样的问题,特别是考虑到Library DR2321 的建议解决方案。)

显然,这是标准中的一个缺陷,因为复制分配传播不相等的分配器必须导致重新分配,这与 23.3.6.3p5 相矛盾。

我们可以期待并希望通过以下方式解决这个缺陷:

  • 在非分配器修改复制分配上的非缩减 capacity()
  • 未指定 capacity() 分配器修改复制分配;
  • 在非分配器传播移动分配上非缩减 capacity()
  • source-container capacity() on allocator-propagating move assignment。

但是,在当前情况下,直到澄清这一点,您最好不要依赖任何特定行为。幸运的是,有一个简单的解决方法可以保证不会减少capacity()

bigVector.assign(littleVector.begin(), littleVector.end());

【讨论】:

  • 容量备注限制了插入的行为。可以说容器分配不属于插入的范畴。
  • @BenVoigt 序列容器复制分配被指定为需要CopyAssignable。很明显,如果初始容量不足,非分配器修改副本分配必须导致重新分配。
  • 我同意。但我不知道学究是否会将该操作称为“插入”并应用上述规则。
  • 我已经开始在 ISO C++ 论坛上讨论:groups.google.com/a/isocpp.org/forum/#!topic/std-discussion/…
【解决方案2】:

operator= 对标准容器的唯一要求是之后的 src == dst,如表 96(在 23.2,一般容器要求中)中所指定。再者,同一张表指定operator ==的含义:

distance(lhs.begin(), lhs.end()) == distance(rhs.begin(), rhs.end()) // same size
  && equal(lhs.begin(), lhs.end(), rhs.begin()) // element-wise equivalent

请注意,这不包括任何方式的容量。除了capacity() &gt;= size() 的一般不变量之外,标准的任何其他部分也没有提到容量。因此,分配后的容量值是未指定的,只要保持分配器要求,容器就可以随意执行分配。


通常,您会发现实现的行为是这样的

  • 如果分配器比较相等并且 dst 有足够的容量,它将保留其旧存储,
  • 否则它将为新元素分配足够的存储空间,并且
  • 在任何情况下都不会关心 src 的容量是多少。

当然,移动分配是另一回事。由于一般是通过窃取源存储来实现的,所以容量也会被占用。

【讨论】:

    【解决方案3】:

    这取决于分配器特征。

    这是http://en.cppreference.com/w/cpp/container/vector/operator%3D的摘录:

    如果 std::allocator_traits::propagate_on_container_copy_assignment() 为真,则目标分配器被源分配器的副本替换。如果目标分配器和源分配器不相等,则使用目标(*this)分配器释放内存,然后在复制元素之前使用其他分配器分配内存。(C++11 起)

    基本上,如果分配器不兼容(如果它们无法释放彼此的内存),则使用新分配器重新分配内存。

    向量实现之间无关紧要,分配器实现之间应该无关紧要(这是有道理的)。

    【讨论】:

    • 您是否有该声明的规范性参考(即来自标准)?
    • 虽然这是正确的,但它不能回答是否在分配器相同时也会发生解除分配,或者不会在容器复制分配上传播。
    • @ecatmur:23.2.1p7 讨论了分配器替换。分配器更换的时刻是容器最后一次被允许使用旧分配器的时间;所有未来的操作都是根据替换分配器定义的。因此,此时它必须释放其旧缓冲区。
    • @BenVoigt 当然,假设它们不相等。
    猜你喜欢
    • 1970-01-01
    • 2011-04-22
    • 2012-06-16
    • 1970-01-01
    • 2013-01-27
    • 2010-12-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多