Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++: vector of pointer loses the reference after push_back()

In my code a have a global vector of Node object and a local vector of Node pointers:

#include<cstdio>
#include<cstdlib>
#include<vector>

using namespace std;

class Node {
    int n;

public:
    Node(int i) : n(i);
    int getN() { return n; }
};

vector<Node> v;

int main() {
    vector<Node*> p;
    v.push_back(Node(1));
    p.push_back(&v[0]);
    printf("first node id : %d\n", (*p[0]).getN());

    return 0;
}

I inserted a node object to global vector & inserted the pointer of that object in the local vector. Output of my above code is:

first node id : 1

However, if I change my main function to this:

int main()
{
    vector<Node*> p;
    v.push_back(Node(1));
    p.push_back(&v[0]);
    v.push_back(Node(2));
    p.push_back(&v[1]);
    printf("first node id : %d\n", (*p[0]).getN());

    return 0;
}

The code prints a garbage value:

first node id : 32390176

I can't figure out the problem. Does the vector data structure changes the references of each object after an insertion ? How can I fix this ?

like image 927
palatok Avatar asked Jan 10 '16 16:01

palatok


People also ask

Does vector Push_back make a copy?

Yes, std::vector<T>::push_back() creates a copy of the argument and stores it in the vector.

What does vector Push_back do?

C++ Vector Library - push_back() Function The C++ function std::vector::push_back() inserts new element at the end of vector and increases size of vector by one.

What does Push_back return?

It does not have any return value.

Does Push_back return an iterator?

The reason is that vector provides an insert method that does return an iterator to the inserted position. The standard is consistent in that push_back doesn't return a value but some forms of insert will, when available and appropriate.


2 Answers

Yes, a push_back() on a vector invalidates all references (and pointers) to elements in that vector if it has to reallocate. There are various ways to work around this. If you know that your vector will have a particular number of nodes, you can use reserve(). In your example, you could reserve two elements:

int main()
{
    v.reserve(2);
    .
    .
    .
}

This will make sure the vector has preallocated enough storage so that it doesn't need to reallocate.

If you don't know the size ahead of time, then you'll have to change something about your approach. You might use a std::deque instead of a std::vector, since std::deque doesn't invalidate references when you use push_back(). You might store indices instead of pointers. Or you might need to push all the nodes into your vector before making the pointers.

int main()
{
    v.push_back(Node(1));
    v.push_back(Node(2));

    vector<Node*> p;
    p.push_back(&v[0]);
    p.push_back(&v[1]);

    printf("first node id : %d\n", (*p[0]).getN());

    return 0;
}
like image 79
Vaughn Cato Avatar answered Sep 21 '22 14:09

Vaughn Cato


"Does a vector change the references after an insertion?"

Possibly, yes. An std::vector may reallocate its (heap) storage when you add/push_back() additional elements, invalidating all pointers:

Iterator [read: Pointer] Invalidation

(for operations) push_back, emplace_back ... If the vector changed capacity, all of them [i.e. all iterators are invalidated]. If not, only end().

"How can I fix this?"

The above invalidation rule does not apply if a vector's capacity does not change due to an insertion - since vectors do not reallocate storage unnecessarily. So if you pre-set your vector's capacity to 2 in your example (say, with v.reserve(2)), the pointer will remain valid. If you don't know the size in advance, but you can delay the construction of the second vector (with the pointers), you don't have to reserve, you'll just have the size after inserting the last element.

The approaches above are highly unrecommended, however. If you were to make your vector constant - at least in the scope of a function in which you would construct and use the second vector - you would have a strong guarantee of non-reallocation. Alternatively, if you could determine the size in advance you might use an std::array, and it would be more fitting to use pointers into that container's storage:

Iterator Invalidation

As a rule, iterators to an array are never invalidated throughout the lifetime of the array.

You might also consider storing indices into your vector (although there, as well, the vector might shrink, invalidating the indices, or you might insert elements in the middle etc).

Anyway, I suspect that you might not actually want to do any of this, i.e. it seems to be a not-so-good solution to a problem which could be handled with a different approach altogether.

PS - If the vector has a custom allocator then everything I've written might be irrelevant.

like image 37
einpoklum Avatar answered Sep 23 '22 14:09

einpoklum