Almost none though this is admittedly a strange answer, and probably nowhere close to suitable for everyone.
But I have found it so much more useful in my personal case to store all instances of a particular type in a central, random-access sequence (thread-safe), and instead to work with 32-bit indices (relative addresses, i.e.), rather than absolute pointers.
For a start:
- It halves the memory requirements of the analogical pointer on 64-bit platforms. So far I've never needed more than ~4.29 billion instances of a particular data type.
- It makes sure that all instances of a particular type,
T
,
will never be too scattered in memory. That tends to reduce cache
misses for all kinds of access patterns, even traversing linked
structures like trees if the nodes are linked together using indices rather than pointers.
- Parallel data becomes easy to associate using cheap parallel arrays (or sparse arrays) instead of trees or hash tables.
- Set intersections can be found in linear-time or better using, say, a parallel bitset.
- We can radix sort the indices and get a very cache-friendly sequential access pattern.
- We can keep track of how many instances how of a particular data type have been allocated.
- Minimizes the number of places that have to deal with things like exception-safety, if you care about that sort of thing.
That said, convenience is a downside as well as type safety. We can't access an instance of T
without having access to both container and index. And a plain old int32_t
tells us nothing about what data type it refers to, so there's no type safety. We could accidentally try to access a Bar
using an index to Foo
. To mitigate the second problem I often do this sort of thing:
struct FooIndex
{
int32_t index;
};
Which seems kind of silly but it gives me back the type safety so that people can't accidentally try to access a Bar
through an index to Foo
without a compiler error. For the convenience side, I just accept the slight inconvenience.
Another thing which could be a major inconvenience for people is that I can't use OOP-style inheritance-based polymorphism, since that would call for a base pointer which can point to all kinds of different subtypes with different size and alignment requirements. But I don't use inheritance much these days -- prefer the ECS approach.
As for shared_ptr
, I try not to use it so much. Most of the time I don't find it makes sense to share ownership, and doing so haphazardly can lead to logical leaks. Often at least at a high-level, one thing tends to belong to one thing. Where I often found it tempting to use shared_ptr
was extending the lifetime of an object in places that didn't really deal with ownership so much, like just a local function in a thread to make sure the object isn't destroyed before the thread is finished using it.
To tackle that problem, instead of using shared_ptr
or GC or anything like that, I often favor short-lived tasks running from a thread pool, and make it so if that thread requests to destroy an object, that the actual destruction is deferred to a safe time when the system can ensure that no thread needs to access said object type.
I do still sometimes end up using ref-counting but treat it like a last resort strategy. And there are a few cases where it genuinely makes sense to share ownership, like the implementation of a persistent data structure, and there I do find it makes perfect sense to reach for shared_ptr
right away.
So anyway, I mostly use indices, and use both raw and smart pointers sparingly. I like indices and the kinds of doors they open up when you know your objects are stored contiguously, and not scattered across the memory space.