3 Ideas You Must Understand to Use C++11 Shared Pointers in Leadwerks Game Engine 5
Leadwerks Game Engine 5 is being designed to make use of shared pointers. This eliminates manual reference counting, which has probably been the most difficult part of programming games with Leadwerks. Here are three concepts you must understand before you start using smart pointers in Leadwerks 5,
Don't Create Multiple Shared Pointers from One Object
When a shared pointer goes out of scope, it deletes the object it references. If another smart pointer was created separately that references that object, the other smart pointer will now point to an object that has been deleted! Set breakpoints in the example below and you will see the problem. The object is deleted while the second smart pointer still references it. Any attempt to use the second smart pointer will cause an error:
class Thing { public: ~Thing(); }; Thing::~Thing() {}// <--------- set a breakpoint here int main(int argc, const char *argv[]) { Thing* thing = new Thing; shared_ptr<Thing> p1 = shared_ptr<Thing>(thing); shared_ptr<Thing> p2 = shared_ptr<Thing>(thing); p1 = nullptr; int k = 0;// <--------- set a breakpoint here }
Instead, initialize a smart pointer once and copy it. Here is the correct way:
class Thing { public: ~Thing(); }; Thing::~Thing() {}// <--------- set a breakpoint here int main(int argc, const char *argv[]) { Thing* thing = new Thing; shared_ptr<Thing> p1 = shared_ptr<Thing>(thing); shared_ptr<Thing> p2 = p1; p1 = nullptr; int k = 0;// <--------- set a breakpoint here }
It's even better to eliminate the new keyword entirely and create object and smart pointer in one step:
class Thing { public: ~Thing(); }; Thing::~Thing() {}// <--------- set a breakpoint here int main(int argc, const char *argv[]) { shared_ptr<Thing> p1 = make_shared<Thing>(); shared_ptr<Thing> p2 = p1; p1 = nullptr; int k = 0;// <--------- set a breakpoint here }
The point is, you create the first smart pointer and thereafter all code should pass that around. You never need to access the pointer directly.
Of course the use of auto makes everything a lot simpler:
auto p1 = make_shared<Thing>(); auto p2 = p1;
Parent / Child Relationships
If you have an object that is some kind of "child" of a parent object, you probably want that parent to keep a smart pointer to the child that keeps the child from being deleted. However, if the child has a smart pointer to the parent you are creating a circular reference that will never be deleted from memory. Think of the parent as the owner of the child. Something other than the child must keep the parent in memory, but sometimes the child wants to retrieve the parent object in a bit of code. Therefore, for the child member we will use a shared pointer, and for the parent member we will use a weak pointer:
class Thing { public: shared_ptr<Thing> child; weak_ptr<Thing> parent; ~Thing(); shared_ptr<Thing> GetParent(); };
The GetParent() function would look like this:
shared_ptr<Thing> Thing::GetParent() { return parent.lock(); }
You can modify existing functions that access a parent by adding one line of code. Here is one such function as it would appear in Leadwerks 4, where the parent member is just a regular old stupid pointer:
void Thing::AccessParent() { if (parent != NULL) { //do some stuff to parent here } }
The updated version for Leadwerks 4 creates a new shared_ptr<Thing> variable (with auto) and locks the weak pointer to make a shared pointer. The rest of the code works seamlessly:
void Thing::AccessParent() { auto parent = this->parent.lock(); if (parent != NULL) { //do some stuff to parent here } }
Class Functions Should Never Returns Themselves
The following code illustrates a problematic issue:
class Thing { public: shared_ptr<Thing> GetSelf() }; shared_ptr<Thing> Thing::GetSelf() { return shared_ptr<Thing>(this); } int main(int argc, const char *argv[]) { shared_ptr<Thing> p1 = make_shared<Thing>(); shared_ptr<Thing> p2 = p1->GetSelf(); }
The GetSelf() function creates a new shared pointer that has no relation to the first one. Both of these shared pointers will attempt to delete the object when they go out scope. Only one will win.
I did a search throughout the entire Leadwerks Engine project and found only three instances of the phrase "return this;". The easiest way to fix this problem would be to eliminate this type of behavior altogether by having a parent get the object instead of the object returning itself. For example if you have a recursive function that is structured like this:
Thing* Thing::FindChild(const std::string& name) { if (name == this->name) return this; for (auto it = kids.begin(); it != kids.end(); it++) { auto child = (*it)->FindChild(name); if (child) return child; } return NULL; }
You can restructure it like this:
shared_ptr<Thing> Thing::FindChild(const std::string& name) { for (auto it = kids.begin(); it != kids.end(); it++) { if (name == (*it)->name) return this; auto child = (*it)->FindChild(name); if (child) return child; } return NULL; }
I considered deriving all complex objects from a "SharedObject" class with a weak pointer that referenced itself:
class SharedObject { weak_ptr<SharedObject> self; };
However, this requires the "self" member to be set when the object is created and is tedious to use. I think it's easier to just eliminate functions that return the object itself.
- 1
3 Comments
Recommended Comments