3

I have a vector< Object > myvec which I use in my code to hold a list of objects in memory. I keep a pointer to the current object in that vector in the "normal" C fashion by using

Object* pObj = &myvec[index];

This all works fine if... myvec doesn't grow big enough that it is moved around during a push_back at which time pObj becomes invalid - vectors guarantee data is sequential, hence they make no effort to keep the vector at the same memory location.

I can reserve enough space for myvec to prevent this, but I dnt' like that solution.

I could keep the index of the selected myvec position and when I need to use it just access it directly, but it's a costly modification to my code.

I'm wondering if iterators keep the their references intact as a vector is reallocated/moved and if so can I just replace

Object* pObj = &myvec[index];

by something like

vector<Object>::iterator = myvec.begin()+index;

What are the implication of this?

Is this doable?

What is the standard pattern to save pointers to vector positions?

Cheers

6502
  • 112,025
  • 15
  • 165
  • 265
André Moreira
  • 1,669
  • 4
  • 21
  • 35
  • Keeping the index is "costly"? Meh. – akappa Oct 31 '11 at 18:12
  • The standard pattern to preserve iterators is to reserve space upfront, or use a container which preserves iterators. There are compromises either way. – Steve Townsend Oct 31 '11 at 18:22
  • You say below that you "use vectors pretty much as arrays", which is exactly what you *should* be doing -- but I have a suspicion that your entire question is the consequence of some bad design decisions. Somehow this shouldn't be a problem at all... it's just hard to tell without knowing the context. – Kerrek SB Oct 31 '11 at 18:41
  • In what sense is using index a "costly modification"? Do you mean that you have too many lines to change? Or the run-time cost of `myvec[i]` is greater than the run-time cost of `*pObj`? – Robᵩ Oct 31 '11 at 18:44

3 Answers3

3

No... using an iterator you would have the same exact problem. If a vector reallocation is performed then all iterators are invalidated and using them is Undefined Behavior.

The only solution that is reallocation-resistant with an std::vector is using the integer index.

Using for example std::list things are different, but also the are different efficiency compromises, so it really depends on what you need to do.

Another option would be to create your own "smart index" class, that stores a reference to the vector and the index. This way you could keep just passing around one "pointer" (and you could implement pointer semantic for it) but the code wouldn't suffer from reallocation risks.

6502
  • 112,025
  • 15
  • 165
  • 265
  • the problem is that lists don't have the [] operator and that sucks as I use vectors pretty much as an array. – André Moreira Oct 31 '11 at 18:22
  • 2
    @Andre: `std::deque` only invalidates iterators if you insert/remove at the middle. `push_back` and `pop_back` will not invalidate. They have `[]` and lookup speed is closer to `vector` than `list`. – Mooing Duck Oct 31 '11 at 18:28
  • @MooingDuck. Well, that seems to be the way to go. Thanks for the help ;) – André Moreira Oct 31 '11 at 18:35
  • @Andre: lists don't have the invalidation problem, though. – Kerrek SB Oct 31 '11 at 18:40
  • 2
    @MooingDuck: From 23.3.3.4 [deque.modifiers]/p1: An insertion at either end of the deque invalidates all the iterators to the deque. p4 in the same section states that `pop_back()` (or erasing the last element) will invalidate the end iterator. – Howard Hinnant Dec 01 '13 at 19:18
  • 1
    @MooingDuck As Howard pointed out, the part about iterator invalidation is incorrect, but replace “iterators” with “references (and pointers)” and it becomes correct. – gx_ Dec 04 '13 at 14:52
3

Iterators are (potentially) invalidated by anything that could resize the vector (e.g., push_back).

You could, however, create your own iterator class that stored the vector and an index, which would be stable across operations that resized the vector:

#include <iterator>
#include <algorithm>
#include <iostream>
#include <vector>

namespace stable {

template <class T, class Dist=ptrdiff_t, class Ptr = T*, class Ref = T&>
class iterator : public std::iterator<std::random_access_iterator_tag, T, Dist, Ptr, Ref>
{
    T &container_;
    size_t index_;
public:
    iterator(T &container, size_t index) : container_(container), index_(index) {}

    iterator operator++() { ++index_; return *this; }
    iterator operator++(int) { iterator temp(*this); ++index_; return temp; }
    iterator operator--() { --index_; return *this; }
    iterator operator--(int) { stable_itertor temp(*this); --index_; return temp; }
    iterator operator+(Dist offset) { return iterator(container_, index_ + offset); }
    iterator operator-(Dist offset) { return iterator(container_, index_ - offset); }

    bool operator!=(iterator const &other) const { return index_ != other.index_; }
    bool operator==(iterator const &other) const { return index_ == other.index_; }
    bool operator<(iterator const &other) const { return index_ < other.index_; }
    bool operator>(iterator const &other) const { return index_ > other.index_; }

    typename T::value_type &operator *() { return container_[index_]; }
    typename T::value_type &operator[](size_t index) { return container_[index_ + index]; }
};

template <class T>
iterator<T> begin(T &container) { return iterator<T>(container, 0); }

template <class T>
iterator<T> end(T &container) { return iterator<T>(container, container.size()); }

}

#ifdef TEST
int main() { 

    std::vector<int> data;

    // add some data to the container:
    for (int i=0; i<10; i++)
        data.push_back(i);

    // get iterators to the beginning/end:
    stable::iterator<std::vector<int> > b = stable::begin(data);
    stable::iterator<std::vector<int> > e = stable::end(data);

    // add enough more data that the container will (probably) be resized:
    for (int i=10; i<10000; i++)
        data.push_back(i);

    // Use the previously-obtained iterators:
    std::copy(b, e, std::ostream_iterator<int>(std::cout, "\n"));

    // These iterators also support most pointer-like operations:
    std::cout << *(b+125) << "\n";
    std::cout << b[150] << "\n";

    return 0;
}
#endif

Since we can't embed this as a nested class inside of the container like a normal iterator class, this requires a slightly different syntax to declare/define an object of this type; instead of the usual std::vector<int>::iterator whatever;, we have to use stable::iterator<std::vector<int> > whatever;. Likewise, to obtain the beginning of a container, we use stable::begin(container).

There is one point that may be a bit surprising (at least at first): when you obtain a stable::end(container), that gets you the end of the container at that time. As shown in the test code above, if you later add more items to the container, the iterator your obtained previously is not adjusted to reflect the new end of the container -- it retains the position it had when you obtained it (i.e., the position that was the end of the container at that time, but isn't any more).

Jerry Coffin
  • 476,176
  • 80
  • 629
  • 1,111
2

No, iterators are invalidated after vector growth.

The way to get around this problem is to keep the index to the item, not a pointer or iterator to it. This is because the item stays at its index, even if the vector grows, assuming of course that you don't insert any items before it (thus changing its index).

Michael Goldshteyn
  • 71,784
  • 24
  • 131
  • 181