There are two facets to this issue:
First, you may want to defer destruction until the end of the frame (or at least until some time after you know all your inter-object interactions will have been resolved). This is fairly straightforward: when you "destroy" an object, put that object into a "pending kill" set and at the end of the frame, iterate the set, actually destroying every object and then emptying the set.
Second is the issue of outstanding references to dead objects from live ones. This is a much larger design issue for object lifetime and there's a lot of ways you can handle it. You need to decide certain things about how your objects will function to choose a solution.
- Does a reference to an object never need to keep that object alive?
- Does a reference to an object always need to keep that object alive?
- Does a reference to an object sometimes need to keep an object alive? Under what conditions, and are those conditions determined at compile time or at run time?
- Or you simply need to be able to tell if a referenced object has been destroyed and consequently prevent accessing it?
(When I say "keep alive" I mean doing so simply by virtue of the reference existing. Whether or not you can request destruction of that object via that reference is another thing altogether.)
If a reference never needs to keep an object alive, you are in the ideal world. This probably means that reference is the only reference to the object, though, and the containing objects entirely controls its lifetime. If you are extremely rigid about the way your objects are created and destroyed you can make this work (always ensuring objects which have non-lifetime-preserving references are destroyed before the things they reference are, in and inside-out fashion), but it takes discipline.
If a reference must always keep an object alive, a reference-counting approach is potentially what you want to look at. You will also want to include some way of making non-counting references (so called "weak pointers," like C++'s
std::weak_ptr) to break cycles or otherwise create non-counting references. You can also look into solutions involving garbage collection, although in some languages (like C++) this would be quite an intensive option. It's possible though (Unreal does it).
If a reference sometimes needs to keep an object alive and those conditions are determined at compile time, a reference-counting approach can still be used, you just choose a weak reference for this instance. If the conditions are determined at compile time, you'd have to use a more complex reference counting approach where a reference can switch from counting to non-counting, and I'd argue this is a sign you need to make your ownership policies more rigid instead.
If a reference simply needs to be able to determine if a thing is destroyed, you're probably in the second-best scenario. This is where you should try to be, in my opinion (reference counting makes overall determinism related to destruction harder to reason about). There are various handle-based solutions to object management that can work here. The crux of this approach is to keep all your objects in one place along with some metadata that includes a "salt" value, and hand out handles which reference those objects. The handle holds a pointer or index that refers to the actual object, and a salt value of its own. Whenever you actually delete an object, you increment the salt for that object's slot in the object storage. That way allows handles to test their salt against the current salt in storage and if they differ, the handle refers to a dead object.
There are various structures that can be used to implement this, such as innumerable variations on a slot map or related data structures like
plf::colony (which can be conceptually adapted to support salted handles instead of iterators or bare pointers).