通过引用同一向量的元素插入到向量中

Inserting into vector by reference to element of same vector

本文关键字:向量 元素 插入 引用      更新时间:2023-10-16

我想知道是否有更有经验的人能够澄清这是否是在向量上执行的错误操作:

std::vector<int> v{1, 2, 3, 4, 5};
v.insert(v.begin() + 1, v[0]);

我问的原因是因为要插入的元素是对向量中第 0 个元素的引用。如果插入强制调整矢量大小(因为其容量已满),则对v[0]的引用将失效,并且代码可能会插入不正确的值。下面是一些可能演示的伪代码:

template <typename T>
void vector_insert_method(Iterator pos, const T& value) {
if capacity full:
reallocate array and copy over element
// if value was reference to elem in this vector,
// that reference might be invalidated

insert element
++size
}

此问题在并发系统上可能更现实。

一个类似且相关的问题是,如果您尝试插入您尝试插入的位置之后的元素,会发生什么。例如,执行类似v.insert(v.begin(), v[2])的操作,因为标准指出对插入点之后元素的引用无效。这能保证有效吗?

最初的问题问...

[..] 这是否是在向量上执行的错误操作:

v.insert(v.begin(), v[0]);
//              ^^^ no + 1 here!

可能是的,这可能是未定义的行为。引用N3337("几乎C++11"AFAIK):

[..]如果未发生重新分配,则插入点之前的所有迭代器和引用仍然有效。[..]

§23.3.6.5/1

虽然这不是阅读标准时习惯的明确措辞,但我将其解释为:迭代器以及对插入点及其之后的引用无效。

因此,对v[0]的引用因调用insert而无效。即使没有发生重新分配,insert也必须将所有元素转移到更高的指数(因此v[0]重新定位为v[1])。

假设元素类型不是微不足道的可破坏的,那么无论重新定位如何完成(通过移动或复制),在从v[0]分配/构造v[1]之后,需要调用v[0]的析构函数(及其生命周期结束)才能将新对象(您要插入的对象)放置在v[0]的内存位置。因此,在这里,您的引用变成了悬而未决的引用,当之后直接使用它来构造新v[0]时,会导致未定义的行为。不,据我所知,这个问题可以通过不构造新对象而是将新对象分配给"旧"对象来规避std::vector::insert()。我不确定std::vector是否要求以这种方式行事,尽管它的元素类型必须CopyInsertable这暗示可能是这种情况。

更新:我已经使用了另一个答案中提供的代码,添加了一些打印调试。这并没有显示我所期望的(破坏一个元素然后访问它),仍然表明标准库实现(此处由 ideone 使用)不符合标准:即使容量足够大它也会使对插入点之前的元素引用无效。这样它就可以规避上述问题(但打破了标准......所以)。

(*):人们可能会争论何时发生无效。这个问题的最佳答案是 IMO 在调用insert函数之前引用是有效的,并且在从该函数返回后(肯定)无效。无效的确切点未指定。


编辑后的问题提出了同样的问题,但进行了极其重要的修改:

v.insert(v.begin() + 1, v[0]);
//                ^^^^^

现在这是一个完全不同的情况。对v[0]的引用不一定会因调用insert而无效,因为插入点在"后面"v[0]。可能出现的唯一问题是std::vector必须重新分配其内部缓冲区,但在这种情况下,以下操作序列应保证正确的行为:

  1. 分配更大容量的新内存
  2. 在新分配的内存区域中的正确索引处构造新元素。
  3. 将元素从旧(太小)内存复制/移动到新分配的内存区域。
  4. 释放旧记忆。

好的,经过很多环顾四周,标准库似乎有义务完成这项工作。这个想法是,由于标准没有明确说这不应该工作,所以它必须工作。

Andrew Koenig 在这里写了一些关于它的文章,并提出了一个解决方案,其中分配新内存,移动元素,然后才解除分配旧内存。

这里和这里还有其他讨论(#526)。

至于第二种情况,似乎标准库(MacOS 上的 clang )也考虑了您尝试在向量中插入对元素的引用的情况,该元素位于您尝试插入的点之后。为了测试这一点,我为名为 Integer 的整数编写了一个包装类,它的行为与 int 完全相同,只是析构函数将其内部值设置为 -5。因此,如果 std::vector 插入没有考虑到这种情况,我们应该看到插入的 -5 而不是实际值。

#include <vector>
#include <iostream>

using std::cout;    using std::endl;
using std::ostream; using std::vector;

class Integer {
public:
Integer() {
x = -1;     // default value
}
~Integer() {
x = -5;     // destructed value. Not 0 so we can clearly see it
}
Integer(int r) {
x = r;
}
Integer(const Integer& other) {
x = other.x;
}
Integer operator=(const Integer& other) {
x = other.x;
return *this;
}
operator int() const{
return x;
}
friend ostream& operator<<(ostream& os, const Integer& thing) {
os << thing.x;
return os;
}
private:
int x;
};

ostream& operator<<(ostream& os, const vector<Integer> &v) {
std::copy(v.begin(), v.end(), std::ostream_iterator<Integer>(os, ", "));
return os;
}
int main() {
std::vector<Integer> ret {18, 7, 4, 24,11};
cout << "Before: " << ret << endl;
ret.insert(ret.begin(), ret[1]);
cout << "After: " <<  ret << endl;
return 0;
}

这为我们提供了正确的输出:

Before: 18, 7, 4, 24, 11, 
After: 7, 18, 7, 4, 24, 11,