【问题标题】:Is it safe to push_back an element from the same vector?push_back 来自同一向量的元素是否安全?
【发布时间】:2013-09-18 06:53:06
【问题描述】:
vector<int> v;
v.push_back(1);
v.push_back(v[0]);

如果第二个 push_back 导致重新分配,则对向量中第一个整数的引用将不再有效。所以这不安全?

vector<int> v;
v.push_back(1);
v.reserve(v.size() + 1);
v.push_back(v[0]);

这样就安全了?

【问题讨论】:

  • 注意:标准提案论坛目前正在讨论。作为其中的一部分,有人给了example implementation of push_back。另一个海报noted a bug in it,它没有正确处理你描述的情况。据我所知,没有其他人认为这不是错误。并不是说这是确凿的证据,只是一种观察。
  • 很抱歉,我不知道该接受哪个答案,因为正确答案仍有争议。
  • 我被要求在下面的第 5 条评论中评论这个问题:stackoverflow.com/a/18647445/576911。我这样做是通过支持当前说的每个答案来做到这一点的:是的,push_back 来自同一向量的元素是安全的。
  • @BenVoigt: 如果您不同意标准所说的,或者即使您同意标准,但认为它说得不够清楚,这始终是您的选择: cplusplus.github.io/LWG/lwg-active.html#submit_issue 我自己选择这个选项的次数比我记得的要多。有时成功,有时不成功。如果您想讨论标准的内容或应该说的内容,那么 SO 不是一个有效的论坛。我们的谈话没有规范意义。但是您可以通过点击上面的链接来获得规范性影响。
  • @Polaris878 如果 push_back 导致向量达到其容量,向量将分配一个新的更大的缓冲区,复制旧数据,然后删除旧缓冲区。然后它将插入新元素。问题是,新元素是对旧缓冲区中刚刚被删除的数据的引用。除非 push_back 在删除之前复制该值,否则它将是一个错误的引用。

标签: c++ vector reference language-lawyer push-back


【解决方案1】:

看起来http://www.open-std.org/jtc1/sc22/wg21/docs/lwg-closed.html#526 将此问题(或与之非常相似的问题)作为标准中的一个潜在缺陷解决了:

1) const 引用获取的参数可以在执行期间更改 函数

例子:

给定 std::vector v:

v.insert(v.begin(), v[2]);

v[2]可以通过移动vector的元素来改变

建议的解决方案是这不是缺陷:

vector::insert(iter, value) 需要工作,因为标准 不允许它不工作。

【讨论】:

  • 我在 17.6.4.9 中找到了权限:“如果函数的参数具有无效值(例如函数域之外的值或对其预期用途无效的指针),则行为是未定义的。”如果发生重新分配,则所有迭代器和对元素的引用都无效,这意味着传递给函数的参数引用也无效。
  • 我认为关键是实现负责进行重新分配。如果最初定义了输入,则它有责任确保定义行为。由于规范明确指定 push_back 进行复制,因此实现必须以牺牲执行时间为代价,在取消分配之前缓存或复制所有值。由于在这个特定问题中没有留下任何外部引用,因此迭代器和引用是否无效并不重要。
  • @NeilKirk 我认为这应该是权威的答案,Stephan T. Lavavej on Reddit 也使用了基本相同的论点。
  • v.insert(v.begin(), v[2]); 无法触发重新分配。那么这如何回答这个问题呢?
  • @ThomasMcLeod:是的,它显然可以触发重新分配。您正在通过插入新元素来扩展矢量的大小。
【解决方案2】:

是的,它是安全的,而且标准库的实现跳过了一些环节来做到这一点。

我相信实施者会以某种方式将此要求追溯到 23.2/11,但我无法弄清楚如何,也找不到更具体的东西。我能找到的最好的是这篇文章:

http://www.drdobbs.com/cpp/copying-container-elements-from-the-c-li/240155771

检查 libc++ 和 libstdc++ 的实现表明它们也是安全的。

【讨论】:

  • 一些支持在这里真的很有帮助。
  • 这很有趣,我必须承认我从未考虑过这种情况,但确实似乎很难实现。它也适用于vec.insert(vec.end(), vec.begin(), vec.end()); 吗?
  • @MatthieuM。否:表 100 说:“pre: i 和 j 不是 a 的迭代器”。
  • 我现在正在投票,因为这也是我的回忆,但需要参考。
  • 在您使用的版本中是 23.2/11 “除非另有说明(明确或通过根据其他函数定义函数),调用容器成员函数或将容器作为参数传递库函数不应使该容器内对象的迭代器无效或更改其值。” ?但vector.push_back 另有说明。 “如果新大小大于旧容量,则会导致重新分配。”和(reserve)“重新分配使所有引用序列中元素的引用、指针和迭代器无效。”
【解决方案3】:

该标准甚至可以保证您的第一个示例是安全的。引用 C++11

[sequence.reqmts]

3 在表 100 和 101 中 ...X 表示序列容器类,a 表示包含 T 类型元素的 X 值,...t 表示左值或X::value_type 的 const 右值

16 表 101 ...

表达式 a.push_back(t) 返回类型 void 操作语义 附加t. 的副本要求: T 应为 CopyInsertableX容器 basic_string, deque, list, vector

因此,即使它不完全是微不足道的,实现也必须保证它不会在执行 push_back 时使引用无效。

【讨论】:

  • 我不明白这如何保证它是安全的。
  • @Angew:它确实使t 无效,唯一的问题是复制之前还是之后。你最后一句话肯定是错的。
  • @BenVoigt 由于t 满足列出的先决条件,因此可以保证所描述的行为。不允许实现使先决条件无效,然后以此为借口不按规定行事。
  • @BenVoigt 客户没有义务在整个通话过程中保持前提条件;只是为了确保在调用开始时满足它。
  • @BenVoigt 这是一个很好的观点,但我相信传递给for_each 的函子需要不使迭代器无效。我无法为for_each 提供参考,但我在一些算法上看到了诸如“op 和 binary_op 不应使迭代器或子范围无效”之类的文字。
【解决方案4】:

第一个示例是否安全并不明显,因为push_back 的最简单实现是首先重新分配向量(如果需要),然后复制引用。

但至少它在 Visual Studio 2010 中似乎是安全的。它对push_back 的实现会在您推回向量中的元素时对这种情况进行特殊处理。 代码结构如下:

void push_back(const _Ty& _Val)
    {   // insert element at end
    if (_Inside(_STD addressof(_Val)))
        {   // push back an element
                    ...
        }
    else
        {   // push back a non-element
                    ...
        }
    }

【讨论】:

  • 我想知道规范是否要求这样做是安全的。
  • 根据标准,它不需要是安全的。但是,可以以安全的方式实现它。
  • @BenVoigt 我想说 is 是安全的(见我的回答)。
  • @BenVoigt 在您通过引用时,它是有效的。
  • @Angew:这还不够。您需要传递一个在通话期间保持有效的引用,而这个没有。
【解决方案5】:

这不是标准的保证,但作为另一个数据点,v.push_back(v[0]) 对于LLVM's libc++ 是安全的。

libc++'s std::vector::push_back 在需要重新分配内存时调用__push_back_slow_path

void __push_back_slow_path(_Up& __x) {
  allocator_type& __a = this->__alloc();
  __split_buffer<value_type, allocator_type&> __v(__recommend(size() + 1), 
                                                  size(), 
                                                  __a);
  // Note that we construct a copy of __x before deallocating
  // the existing storage or moving existing elements.
  __alloc_traits::construct(__a, 
                            _VSTD::__to_raw_pointer(__v.__end_), 
                            _VSTD::forward<_Up>(__x));
  __v.__end_++;
  // Moving existing elements happens here:
  __swap_out_circular_buffer(__v);
  // When __v goes out of scope, __x will be invalid.
}

【讨论】:

  • 不仅必须在释放现有存储空间之前进行复制,还必须在从现有元素移动之前进行复制。我想现有元素的移动是在__swap_out_circular_buffer 中完成的,在这种情况下,这个实现确实是安全的。
  • @BenVoigt:说得好,您确实是正确的,移动发生在__swap_out_circular_buffer 内部。 (我添加了一些 cmets 来说明这一点。)
【解决方案6】:

第一个版本肯定不安全:

通过调用标准库容器或字符串成员函数获得的迭代器上的操作可以访问底层容器,但不得修改它。 [注意:特别是,使迭代器无效的容器操作与与该容器关联的迭代器上的操作冲突。 — 结束注释]

来自第 17.6.5.9 节


请注意,这是关于数据竞争的部分,人们通常认为它与线程结合在一起......但实际定义涉及“发生在之前”的关系,我看不到多方之间的任何排序关系 - push_back 的影响在这里发挥作用,即引用失效似乎没有被定义为相对于复制构造新尾部元素的顺序。

【讨论】:

  • 应该理解是注释,不是规则,所以它是在解释上述规则的结果......并且引用的结果是相同的。
  • v[0] 的结果不是迭代器,同样,push_back() 不带迭代器。所以,从语言律师的角度来看,你的论点是无效的。对不起。我知道,大多数迭代器都是指针,而使迭代器无效的点与引用几乎相同,但您引用的标准部分与当前情况无关。
  • -1。这是完全不相关的报价,无论如何也不回答。委员会说x.push_back(x[0]) 是安全的。
【解决方案7】:

完全安全。

在您的第二个示例中,您有

v.reserve(v.size() + 1);

这不是必需的,因为如果向量超出其大小,它将暗示reserve

Vector 负责这些东西,而不是你。

【讨论】:

    【解决方案8】:

    两者都是安全的,因为 push_back 将复制值,而不是引用。如果您要存储指针,就向量而言,这仍然是安全的,但只需知道向量的两个元素将指向相同的数据。

    第 23.2.1 节一般容器要求

    16
    • a.push_back(t) 附加 t 的副本。要求:T 应可复制插入到 X 中。
    • a.push_back(rv) 附加 rv 的副本。要求:T 应可移动插入到 X 中。

    因此 push_back 的实现必须确保插入 v[0] 的副本。举个反例,假设一个实现会在复制之前重新分配,它肯定不会附加 v[0] 的副本,因此违反了规范。

    【讨论】:

    • push_back 也会resize 向量,在一个简单的实现中,这将在复制发生之前invalidate 引用。因此,除非您可以通过引用标准来支持这一点,否则我会认为它是错误的。
    • “this”是指第一个例子还是第二个例子? push_back 会将值复制到向量中;但是(据我所见)可能会发生重新分配之后,此时它试图复制的引用不再有效。
    • push_back 接收它的参数by reference
    • @OlivierD:它必须 (1) 分配新空间 (2) 复制新元素 (3) 移动构建现有元素 (4) 销毁移动的元素 (5) 免费旧存储——按那个顺序——使第一个版本工作。
    • @BenVoigt 如果容器无论如何都要完全忽略该属性,为什么还要要求一个类型是 CopyInsertable?
    【解决方案9】:

    从 23.3.6.5/1:Causes reallocation if the new size is greater than the old capacity. If no reallocation happens, all the iterators and references before the insertion point remain valid.

    由于我们在末尾插入,所以没有引用将无效如果向量没有调整大小。因此,如果向量的capacity() &gt; size() 则保证可以工作,否则保证为未定义行为。

    【讨论】:

    • 我相信规范实际上保证了这在任何一种情况下都能正常工作。不过,我正在等待参考。
    • 问题中没有提到迭代器或迭代器安全性。
    • @OlivierD 迭代器部分在这里是多余的:我对引用的references 部分感兴趣。
    • 实际上保证是安全的(见我的回答,push_back 的语义)。
    猜你喜欢
    • 2014-02-05
    • 2016-01-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-12-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多