重新分配 std::vector 对象时指向内部数据结构的指针的有效性

Validity of pointers to internal data structure when reallocating a std::vector object

假设有一个 class A 包含一个整数向量。现在假设创建了 A 的向量。例如,如果由于 push_back 而发生 A 对象的重新分配(因此向量对象被移动),指向 int 本身的指针是否仍然有效?这个有保证吗?

澄清一下:

class A {
public:
    A() {};
    std::vector<int> a = {1,2,3,4,5,6,7,8,9};
};

int main()
{
    std::vector<A> aVec(2);

    int *x1 = &(aVec[1].a[2]);
    A *x2 = &aVec[1];
    std::cout << x1 << " - " << x2 << " - " << aVec.capacity() << "\n";

    aVec.resize(30);

    int *y1 = &(aVec[1].a[2]);
    A *y2 = &aVec[1];
    std::cout << y1 << " - " << y2 << " - " << aVec.capacity() << "\n";
}

运行这段代码得到:

0x1810088 - 0x1810028 - 2
0x1810088 - 0x18100c8 - 30

所以它表明指针仍然有效。但我想确保这是有保证的,而不仅仅是偶然的事情。我倾向于说这是有保证的,因为向量的内部数据是动态分配的,但是,再次,只是想检查一下。

我看过这里 [Iterator invalidation rules] 但它没有考虑这种特定情况(即矢量对象本身的重新分配)。

更新:

我试过这个来检查我在 Jarod42 的回答的评论中写的内容:

std::vector<std::vector<int>> aVec(2, {1,2,3});

int *x1 = &(aVec[1][2]);
std::vector<int> *x2 = &aVec[1];
std::cout << x1 << " - " << x2 << " - " << aVec.capacity() << "\n";

aVec.resize(30);

int *y1 = &(aVec[1][2]);
std::vector<int> *y2 = &aVec[1];
std::cout << y1 << " - " << y2 << " - " << aVec.capacity() << "\n";

得到这个:

0x16f0098 - 0x16f0048 - 2
0x16f0098 - 0x16f00c8 - 30

这对我来说很奇怪。我期望 x2==y2.

这取决于您 A 的移动构造函数。 但是因为它 (*),它将使用 vector<int> 的移动构造函数作为 a,并且根据 http://www.cplusplus.com/reference/vector/vector/vector/

[..] no elements are constructed (their ownership is directly transferred).

所以指向整数本身的指针仍然有效。

Edit: (*) A 应该是 noexceptstd::vector 不保证是 noexcept.

您的 A 元素将尽可能移动,并且 A 具有隐式移动构造函数和隐式移动赋值运算符,因此成员向量也将被移动。

现在,移动矢量不一定等同于 a.swap(b),因此如果您想要 保证,则不能依赖隐式移动函数;你可以自己写。

但是无论您自己保证还是通过查找特定标准库实现的代码获得保证,您都可以确信指向各个元素的指针和迭代器将保持有效:

[C++11: 23.2.1/8]: The expression a.swap(b), for containers a and b of a standard container type other than array, shall exchange the values of a and b without invoking any move, copy, or swap operations on the individual container elements. [..]

遗憾的是,这并不能保证。也就是说,所有 3 个当前实现(libc++、libstdc++ 和 VS-2015)似乎都可以保证这一点。问题是 A 的移动构造函数是否为 noexcept:

static_assert(std::is_nothrow_move_constructible<A>::value, "");

A 的移动构造函数是编译器提供的,因此依赖于 std::vector<int> 的移动构造函数。如果 std::vector<int> 的移动构造函数是 noexcept,那么 A 的移动构造函数是 noexcept,否则不是。

当前草案 N4296 没有将 vector 的移动构造函数标记为 noexcept。但是它允许实现这样做。

这一行:

aVec.resize(30);
如果移动构造函数是 noexcept,

将使用 A 的移动构造函数,否则它将使用 A 的复制构造函数。如果它使用 A 的复制构造函数,则整数的位置将会改变。如果它使用 A 的移动构造函数,整数的位置将保持稳定。

libc++ 和 libstdc++ 将 vector 的移动构造函数标记为 noexcept。因此给 A 一个 noexcept 移动构造函数。

VS-2015 说 A 没有 noexcept 移动构造函数:

static_assert(std::is_nothrow_move_constructible<A>::value, "");

不编译。

尽管如此,VS-2015 并未将整数重新分配到新地址,因此看起来它不符合 C++11 规范。

如果更改 libc++ headers 使得 vector 移动构造函数未标记为 noexcept,则整数确实会重新分配。

委员会最近的讨论表明,每个人都赞成标记 vector noexcept 的移动构造函数(也许 basic_string 也是,但不是其他容器)。因此,未来的标准可能会保证您寻求的稳定性。与此同时,如果:

static_assert(std::is_nothrow_move_constructible<A>::value, "");

编译,那么你有你的保证,否则你没有。

更新

更新中出现x2 != y2的原因是这些是vector<vector<int>>vector<int>的地址。这些内部元素必须找到一个新的(更大的)缓冲区来居住,就像内部元素是 int 一样。但与 int 不同的是,内部元素 vector<int> 可以使用移动构造函数移动到那里(int 必须复制)。但是无论是移动还是复制,内部元素的地址都必须改变(从旧的小缓冲区到新的大缓冲区)。此行为与问题的原始部分一致(内部元素也显示为更改地址)。

是的,LWG 2321 is involved, though not a contentious point. In my answer I've already assumed LWG 2321 已经过去了。除了过度急于调试迭代器无缘无故地(和错误地)使自己失效之外,真的没有其他方法可以让事情发生。 Non-debugging 迭代器永远不会失效,指针或引用也不会。

希望我能够轻松创建带有指向缓冲区的箭头的动画。那真的很清楚。我只是不知道如何在可用的时间内轻松地做到这一点。