One of the most tricky things about C++, as opposed to a garbage-collected language, is the lifetime of a pointer (or the memory it points to). The standard cases are the ones beginners learn pretty quickly:
- “Forever”: Static storage means global lifetime
- Short: Local storage means until the end of the current scope block
- Very short: Objects can be temporarily created and destroyed from passing, return values, or operator overloads.
- Undefined: Manually allocated and destroyed memory
A more subtle type of pointer invalidation is when storing objects in a parent structure. For example, observe the following code:
std::vector<int> v; v.push_back(1); int &x0 = v; printf("%d\n", x0); v.push_back(2); printf("%d\n", x0);
Running this little code block on gcc-4.1, I get:
What happened here? I took an address (references, of course, are just addresses) to a privately managed and stored object (in this case, the object is an integer). When I changed the structure by adding a new number, it allocated new memory internally and the old pointer suddenly became invalid. It still points to readable memory, but the contents are wrong; in a luckier situation it would have crashed.
What’s not always obvious about this type of mistake is that the vector is not being explicitly reorganized, as would be the case with a call like clear(). Instead, it’s important to recognize that if you’re going to cache a local address to something, you need to fully understand how long that pointer is guaranteed to last (if at all).
This type of error can occur when aggressively optimizing. Imagine if the previous example used string instead of int — suddenly, storing a local copy of the string on the stack involves an internal new call, since string needs dynamic memory. To avoid unnecessary allocation, a reference or pointer can be used instead. But if you’re not careful, a simple change will render your cached object unusable — you must update the cached pointers after changes.
There was a was very typical error in SourceMod that demonstrated this mistake. SourceMod has a caching system for lump memory allocations. You allocate memory in the cache and the cache returns an index. The index can give you back a temporary pointer. Observe the following pseudo-code:
int index = cache->allocate(sizeof(X)); X *x = (X *)cache->get_pointer(index); //...code... x->y_index = cache->allocate(sizeof(Y));
Can you spot the bug? By the time allocate() returns, the pointer to x might already be invalidated. The code must be:
int index = cache->allocate(sizeof(X)); X *x = (X *)cache->get_pointer(index); //...code... int y_index = cache->allocate(sizeof(Y)); x = (X *)cache->get_pointer(index); x->y_index = y_index
There are many ways to create subtle crash bugs by not updating cached pointers, and they often go undetected if the underlying allocation doesn’t trigger address changes very often. Worse yet, whether that happens or not is often very dependent on the hardware or OS configuration, and serious corruption or crash bugs may live happily undiscovered for long periods of time.
The moral of this story is: Be careful when keeping pointers to where you don’t directly control the memory. Even if you’ve written the underlying data structure, make sure you remember exactly what the pointer lifetime is guaranteed to be.