Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c++

stl

vector

Assume there is a class A that contains a vector of ints. Now assume that a vector of A's is created. If a reallocation of an A object occurs (so the vector object is moved) due to a push_back for example, will pointers to the ints themselves remain valid? Is this guaranteed?

To clarify:

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";
}

Running this code gives this:

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

so it shows that the pointers remain valid. But I want to make sure that this is guaranteed and not just a chance thing. I'm leaning towards saying that this is guaranteed since the vector's internal data is dynamically allocated, but, again, just wanted to check.

I have looked here [ Iterator invalidation rules ] but it doesn't consider this specific case (i.e. reallocation of the vector object itself).

UPDATE:

I tried this to check what I wrote in the comments for Jarod42's answer:

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";

and got this:

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

which is strange to me. I expected x2==y2.

like image 485
elatalhm Avatar asked Jan 01 '15 23:01

elatalhm


1 Answers

Unfortunately this is not guaranteed. That being said, it is the case that all 3 current implementations (libc++, libstdc++ and VS-2015) appear to guarantee it. The question is whether or not the move constructor for A is noexcept:

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

The move constructor for A is compiler supplied, and thus dependent upon the move constructor of std::vector<int>. If the move constructor of std::vector<int> is noexcept, then the move constructor for A is noexcept, else it is not.

The current draft N4296 does not mark the move constructor for vector as noexcept. However it allows implementations to do so.

This line:

aVec.resize(30);

Will use A's move constructor if that move constructor is noexcept, else it will use A's copy constructor. If it uses A's copy constructor, the location of the ints will change. If it uses A's move constructor, the location of the ints will remain stable.

libc++ and libstdc++ mark vector's move constructor as noexcept. And thus give A a noexcept move constructor.

VS-2015 says that A does not have a noexcept move constructor:

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

does not compile.

Nevertheless, VS-2015 does not reallocate the ints to a new address, and thus it looks like it is not conforming to the C++11 spec.

If one changes the libc++ headers such that the vector move constructor is not marked noexcept, then the ints do indeed reallocate.

Recent discussions on the committee indicate that everyone is in favor of marking the move constructor of vector noexcept (and maybe basic_string too, but not other containers). So it is possible that a future standard will guarantee the stability you seek. In the meantime, if:

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

compiles, then you have your guarantee, else you don't.

Update

The reason that x2 != y2 in the update is that these are the addresses of vector<int> in the vector<vector<int>>. These inner elements had to find a new (bigger) buffer to live in, just the same as if the inner element was int. But unlike int, the inner element vector<int> could move there with a move constructor (int had to copy). But whether moving or copying, the address of the inner element had to change (from the small old buffer to the big new buffer). This behavior is consistent with the original part of the question (where the inner element is also shown to change addresses).

And yes, LWG 2321 is involved, though not a contentious point. In my answer I've already assumed LWG 2321 has passed. There's really no other way for things to happen aside from overly eager debugging iterators to gratuitously (and incorrectly) invalidate themselves. Non-debugging iterators would never invalidate, and neither will pointers or references.

Wish I had the ability to easily create an animation with arrows to buffers. That would be really clear. I just don't know how to easily do that in the time I have available.

like image 104
Howard Hinnant Avatar answered Oct 13 '22 22:10

Howard Hinnant