通过引用相同向量的元素插入向量

时间:2017-04-15 19:33:20

标签: c++ vector reference

我想知道更有经验的人是否能够澄清这是否是对矢量进行的错误操作:

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])之类的操作,因为标准指出插入点后对元素的引用无效。这可以保证有效吗?

2 个答案:

答案 0 :(得分:6)

最初的问题是......

  

[..]这是否是对矢量执行的错误操作:

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[1]之后已经从v[0]分配/构造,v[0]的析构函数需要在新对象(您要插入的对象)之前被调用(并且它的生命周期结束)被放置在v[0]的内存位置。因此,在这里你的引用变成了一个悬空引用,当它直接用于构造新的v[0]时会导致未定义的行为。不,据我所知,这个问题可以通过让std::vector::insert()构建新对象来避开,而是新对象分配给“旧” “一个。我不确定是否要求std::vector以这种方式行事,尽管其元素类型必须为CopyInsertable,这提示可能是这种情况。

另一个答案中提供了

更新: I've played around with the code,添加了一些打印调试功能。这显示我期待的东西(破坏一个元素然后访问它)仍然显示标准库实现(由ideone使用)不< / strong>符合标准:即使容量足够大,它也会在插入点之前使对元素的引用无效。这样就可以避免上述问题(但打破了标准......)。

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

编辑过的问题提出了同样的问题,但有一个非常重要的修改:

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

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

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

答案 1 :(得分:3)

好的,经过大量的调查,似乎标准库有义务使这项工作。这个想法是因为标准没有明确说明这不起作用,所以它必须有效。

Andrew Koenig在这里写了一些关于它的内容并提出了一个解决方案,其中分配了新的内存,元素移过来,然后才会释放旧的内存。

还有其他讨论herehere (#526)

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