将相同向量中的元素push_back安全吗?


126
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]);

这样安全吗?


4
注意:标准提案论坛中目前有一个讨论。作为其中的一部分,有人给出了的示例实现push_back。另一位发帖人指出其中存在一个错误,即它无法正确处理您描述的情况。据我所知,没有人认为这不是错误。并不是说那是确凿的证据,只是一个观察。
本杰明·林德利2013年

9
很抱歉,我不知道接受哪个答案,因为关于正确答案仍存在争议。
尼尔·柯克

4
第5条评论要求我对此问题发表评论:stackoverflow.com/a/18647445/576911。我这样做是通过赞成当前回答的每个答案:是的,从同一个向量中推入一个元素是安全的。
Howard Hinnant 2013年

2
@BenVoigt:<shrug>如果您不同意该标准的内容,或者即使您同意该标准,但又认为它不够清楚,这始终是您的选择: cplusplus.github.io/LWG/ lwg-active.html#submit_issue 我自己采取此选项的次数超过了我的记忆。有时成功,有时不成功。如果您想辩论标准的内容或应该说的内容,那么SO不是有效的论坛。我们的谈话没有规范意义。但是,通过点击上面的链接,您可能会有规范的影响。
Howard Hinnant 2013年

2
@ Polaris878如果push_back使向量达到其容量,则向量将分配一个更大的新缓冲区,复制旧数据,然后删除旧缓冲区。然后它将插入新元素。问题是,新元素是对刚刚删除的旧缓冲区中数据的引用。除非push_back在删除之前复制该值,否则将是错误的引用。
尼尔·柯克

Answers:


31

看起来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 :: insert(iter,value),因为该标准未授予其无效的许可。


我在17.6.4.9中找到了权限:“如果函数的参数具有无效的值(例如,函数域之外的值或对于其预期用途无效的指针),则行为是不确定的。” 如果发生重新分配,则所有迭代器和对元素的引用都将无效,这意味着传递给函数的参数引用也将无效。
Ben Voigt

4
我认为关键是实现负责重新分配。如果最初定义了输入,则有责任确保定义行为。由于规范中明确指定了push_back进行复制,因此实现必须在取消分配之前缓存或复制所有值,而这可能会花费执行时间。由于在此特定问题中没有剩余的外部引用,因此迭代器和引用是否无效也无关紧要。
OlivierD

3
@NeilKirk我认为这应该是权威性的答案,Stephan T. Lavavej 在Reddit上也使用基本相同的论点进行了提及。
TemplateRex

v.insert(v.begin(), v[2]);无法触发重新分配。那么这如何回答这个问题呢?
ThomasMcLeod

@ThomasMcLeod:是的,显然可以触发重新分配。您正在通过插入新元素来扩展向量的大小。
紫罗兰色长颈鹿

21

是的,它很安全,标准库实现也是如此。

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

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

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


9
一些支持在这里确实有帮助。
克里斯

3
这很有趣,我必须承认我从未考虑过这种情况,但确实似乎很难实现。它也适用vec.insert(vec.end(), vec.begin(), vec.end());吗?
Matthieu M.

2
@MatthieuM。否:表100说:“ pre:i和j都不是a的迭代器”。
塞巴斯蒂安·雷德尔2013年

2
我现在正在投票,因为这也是我的回忆,但是需要参考。
bames53 2013年

3
在您使用的版本中是23.2 / 11,“除非另外指定(显式指定或通过其他函数定义一个函数),调用容器成员函数或将容器作为参数传递给库函数都不会使迭代器无效或更改该容器内对象的值。” ?但是是否vector.push_back另有规定。“如果新大小大于旧容量,则会导致重新分配。” 和(at reserve)“重新分配会使引用序列中元素的所有引用,指针和迭代器无效。”
Ben Voigt 2013年

13

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

[sequence.reqmts]

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

16表101 ...

表达式 a.push_back(t) 返回类型 void 操作语义追加的副本t. 要求: TCopyInsertableX集装箱 basic_stringdequelistvector

因此,即使它并非完全无关紧要,实现也必须确保在执行时不会使引用无效push_back


7
我不知道这怎么保证它是安全的。
jrok 2013年

4
@Angew:绝对不会使它无效t,唯一的问题是在复制之前还是之后。您的最后一句话肯定是错误的。
Ben Voigt 2013年

4
@BenVoigt由于t满足列出的前提条件,因此可以保证上述行为。不允许实现使前提条件无效,然后以该前提条件为借口以不按指定的方式行事。
bames53 2013年

8
@BenVoigt客户没有义务在整个调用过程中保持前提;只是为了确保在呼叫开始时得到满足。
bames53 2013年

6
@BenVoigt很好,但是我相信传递给函子的函子for_each不能使迭代器无效。我无法提供的参考for_each,但我在某些算法上看到“ op和binary_op不会使迭代器或子范围无效”的文字。
bames53 2013年

7

显然第一个示例是安全的,因为的最简单实现是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
                    ...
        }
    }

8
我想知道规范是否要求这样做是安全的。
Nawaz 2013年

1
根据标准,不需要安全。但是,可以以安全的方式实施它。
Ben Voigt 2013年

2
@BenVoigt我想说这必须安全的(请参阅我的回答)。
Angew不再为SO

2
@BenVoigt在传递引用时,它是有效的。
Angew不再为SO

4
@Angew:这还不够。您需要传递一个在通话期间保持有效的引用,而这个引用则无效。
Ben Voigt 2013年

3

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

libc ++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中完成的,在这种情况下,此实现确实是安全的。
Ben Voigt

@BenVoigt:好点,您确实正确地知道内部发生了移动__swap_out_circular_buffer。(我要添加一些注释来说明这一点。)
Nate Kohl 2013年

1

第一个版本绝对不安全:

通过调用标准库容器或字符串成员函数获得的迭代器上的操作可以访问基础容器,但不得对其进行修改。[注意:特别是,使迭代器无效的容器操作与对该容器关联的迭代器上的操作发生冲突。—尾注]

从第17.6.5.9节开始


请注意,这是关于数据竞争的部分,人们通常将其与线程结合使用……但是实际定义涉及“先于发生”的关系,并且我看不到push_backin 的多个副作用之间的任何排序关系。在这里玩,也就是说,参考无效似乎没有定义为相对于复制构造新尾元素的顺序。


1
应该理解,这只是一个注释,而不是一条规则,因此它是在解释上述规则的结果……其结果与参考文献相同。
Ben Voigt 2013年

5
的结果v[0]不是迭代器,同样,push_back()不采用迭代器。因此,从语言律师的角度来看,您的论点是无效的。抱歉。我知道,大多数迭代器都是指针,并且使迭代器无效的要点与引用几乎相同,但是您引用的标准部分与当前情况无关。
cmaster-恢复莫妮卡

-1。这是完全无关的报价,反正也不会回答。委员会说x.push_back(x[0])是安全的。
Nawaz 2013年

0

这是完全安全的。

在第二个示例中,您有

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

这是不需要的,因为如果vector超出其大小,则意味着reserve

Vector负责这件事,而不是您。


-1

两者都是安全的,因为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]因此违反了规范。


2
push_back但是,它也会调整向量的大小,并且在幼稚的实现中,这将在复制发生之前使引用无效。因此,除非您可以通过引用标准来对此进行支持,否则我将认为它是错误的。
Konrad Rudolph 2013年

4
“这个”是指第一个例子还是第二个例子?push_back将值复制到向量中;但是(据我所知)重新分配可能会发生,这时它尝试从中复制的引用不再有效。
Mike Seymour 2013年

1
push_back通过引用接受其论点。
bames53 2013年

1
@OlivierD:它必须(1)分配新的空间(2)复制新元素(3)移动构造现有元素(4)销毁移出的元素(5)释放旧存储-以那种顺序-使第一个版本正常工作。
Ben Voigt 2013年

1
@BenVoigt如果容器要完全忽略该属性,为什么容器还要要求该类型为CopyInsertable?
OlivierD 2013年

-2

从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() > size()则可以保证能正常工作,否则,它将保证是未定义的行为。


我相信规范实际上保证了这两种情况下都能正常工作。我正在等待参考。
bames53 2013年

问题中没有提及迭代器或迭代器安全性。
OlivierD 2013年

3
@OlivierD这里的迭代器部分是多余的:我references对引号的部分感兴趣。
Mark B

2
实际上可以保证它是安全的(请参阅我的回答,的语义push_back)。
Angew不再为2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.