Working with Smart Pointers
Some Basics#
In UNIGINE instances of C++ API classes (such as: Node, Mesh, Body, Image and so on...) only store pointers to instances of internal C++ classes, they cannot be created and deleted via the standard new / delete operators. So they should be declared as smart pointers (Unigine::Ptr) that allow you to automatically manage their lifetime. UNIGINE has its own optimized memory allocator for faster and more efficient memory management. Each smart pointer stores a reference counter, i.e. how many smart pointers are pointing to the managed object; when the last smart pointer is destroyed, the counter goes to 0, and the managed object is then automatically deleted.
Not all methods of Engine's internal C++ classes are exposed to the user, some of them are used by the Engine only. These are specific functions that either are used only for some internal purposes, or cannot be given to the user "as is". So, to filter out such methods an intermediate level, called interface, is used. This interface stores a pointer to the instance of the Engine's internal C++ class, a set of wrapper-methods and a set of ownership management methods.
Ownership of each internal class instance matters on its deletion and can be managed using grab() and release() methods. An internal class instance can be owned by:
- Script
- Engine Editor
- API interface class
To create an instance of an internal class we should declare a smart pointer for it and call the create() method - class constructor - providing construction parameters if necessary.
// instantiating an object of an internal class
<Class>Ptr instance = <Class>::create(<construction_parameters>);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// interface level (ownership management)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// check ownership
instance->isOwner();
// release ownership
instance->release();
// grab ownership
instance->grab();
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// pointer level
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// clears the smart pointer
instance.clear();
// destroys the smart pointer
instance.destroy();
The following code illustrates creation of new NodeDummy and SoundSource nodes:
// creating a new dummy node
NodeDummyPtr ND1 = NodeDummy::create();
// create a new sound source using the specified audio file
SoundSourcePtr s = SoundSource::create("my_audio.mp3");
You should avoid cyclic references! If there is a ring, or cycle, of objects that have smart pointers to each other, they keep each other "alive" - they won't get deleted even if no other objects in the universe are pointing to them from "outside" of the ring. This cycle problem is illustrated in the diagram below that shows a container of smart pointers pointing to three objects each of which also point to another object with a smart pointer and form a ring. If we empty the container of smart pointers, the three objects won't get deleted, because each of them still has a smart pointer pointing to them.
See Also#
- For more information on ownership management, see Memory Management page.
- For more information on managing smart pointers, see Ptr class page.
Initializing Pointers: What's Under the Hood?#
Let's get a bit more in-depth, to have a clear understanding of how things work. Generally, there are two ways we can initialize a smart pointer:
Creating a New Instance#
Suppose we created a NodeDummy via the following code:
NodeDummyPtr dummy = NodeDummy::create();
At this very moment 3 objects are actually created:
- A NodeDummy node itself (internal). Its implementation is hidden from the user.
- A NodeDummyInterface - representing a user interface. It stores a pointer to the instance of Engine's internal NodeDummy C++ class and a set of wrapper-methods like:
void setEnabled(int enabled) { obj->setEnabled(enabled); }
It is this user interface that has grab(), release(), isOwner() methods. If an interface is created via the static function create(), after its creation isOwner() == 1 (this means that when you delete this interface, Engine's internal object will also be deleted along with it).
In all other cases (e.g. World::get()->loadNode()), isOwner() == 0.
A NodeDummyPtr smart pointer, which stores a pointer to the NodeDummyInterface which in turn points to the Engine's internal NodeDummy. It has a pointer and a counter and behaves just like an ordinary smart pointer.
If we make a copy like this:
the counter will be set to 2, and NodeDummyInterface shall not be deleted until both these NodeDummyPtr's (dummy and dummy2) call their destructors.NodeDummyPtr dummy2 = dummy;
Other Cases#
Suppose we need to upcast our NodeDummy to a NodePtr, so we use the following code:
NodePtr node = dummy->getNode();
What happens in this case? Again objects are created, but only 2 of them:
- NodeInterface pointing to the internal NodeDummy object. For this interface isOwner() == 0. So should we delete it, the internal object shall not be deleted with it.
- NodePtr pointing to the NodeInterface.
When we initialize pointers using methods with corresponding return values, like:
NodePtr node = Editor::get()->getNodeByName("node_name");
Ownership: When do we grab() and release()?#
You should use grab() and release() methods when you want to transfer ownership to a script, to the Editor, or from one interface to another. Let us illustrate that with an example in C++. Suppose we have a vector Vector<PlayerPtr> players, where we want to store all cameras created in our application. Suppose two cameras (PlayerSpectator and PlayerActor) are to be created and added to the vector in separate functions:
Vector<PlayerPtr> players;
void createMySpectator()
{
PlayerSpectatorPtr spectator = PlayerSpectator::create();
players.append(spectator->getPlayer());
}
void createMyActor()
{
PlayerActorPtr actor = PlayerActor::create();
players.append(actor->getPlayer());
}
In this case, as you've probably guessed, after leaving the scopes of createMySpectator() and createMyActor() functions, both interfaces PlayerSpectatorInterface and PlayerActorInterface will be destroyed along with the internal nodes created. As a result, the elements of the players vector will become bad pointers. To avoid this we should use grab() and release(), everything is simple:
- grab() - sets the ownership flag to 1
- release() - sets the ownership flag to 0
So, the correct code should look like this:
Vector<PlayerPtr> players;
void createMySpectator()
{
PlayerSpectatorPtr spectator = PlayerSpectator::create();
spectator->release();
PlayerPtr player = spectator->getPlayer();
player->grab();
players.append(player);
}
void createMyActor()
{
PlayerActorPtr actor = PlayerActor::create();
actor->release();
PlayerPtr player = actor->getPlayer();
player->grab();
players.append(player);
}
Upcasting and Downcasting: How?#
Sometimes (e.g. when we use Editor::getNode(), World::getNode(), etc. ) we get a NodePtr value, which is a pointer to the base class, but in order to perform operations with certain object (e.g. ObjectMeshDynamicPtr) we need to perform downcasting (i.e. convert from a pointer-to-base to a pointer-to-derived).
Sometimes you may also need to perform upcasting (i.e. convert from a pointer-to-derived to a pointer-to-base), in this case you can use corresponding methods of the derived class.
The code samples below demonstrate the points described above.
Example 1
#include <UnigineEditor.h>
using namespace Unigine;
/* .. */
// find a pointer to node by a given name
NodePtr baseptr = Editor::get()->getNodeByName("my_meshdynamic");
// cast a pointer-to-derived from pointer-to-base
ObjectMeshDynamicPtr derivedptr = ObjectMeshDynamic::cast(baseptr);
// cast a pointer-to-derived from pointer-to-base
ObjectMeshDynamicPtr derivedptr = ObjectMeshDynamic::cast(Editor::get()->getNodeByName("my_meshdynamic"));
// upcast to the pointer to the Object class which is a base class for ObjectMeshDynamic
derivedptr->getObject();
// upcast to the pointer to the Node class which is a base class for all scene objects
derivedptr->getNode();
Example 1
// create a Socket
SocketPtr socket = Socket::create();
// upcast to the pointer to the Stream class which is a base class for Socket
StreamPtr stream = socket->getStream();
Deleting Objects#
A smart pointer has a couple of destructors, both clear their pointer and delete the object (interface in this case), with the only difference:
- destroy() deletes the object immediately.When isOwner() == 1, both interface and internal class instance are deleted, otherwise only the interface instance is deleted.
- clear() deletes the object only in case if the smart pointer calling this method is the last one pointing to the object (interface, in this case). This should be taken into account.
The following example illustrates the difference between clearing and destroying smart pointers:
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Case 1: clearing a smart pointer
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// creating a NodeDummy (now reference counter = 1)
NodeDummyPtr ND1 = NodeDummy::create();
// setting the second pointer to point to the created NodeDummy (now reference counter = 2)
NodeDummyPtr ND2 = ND1;
// clearing ND1 pointer (now reference counter = 1)
ND1.clear();
// ND2 still has the object and we can manage it
ND2->setEnabled(1);
// clearing ND2 pointer (now reference counter = 0 and the object will be deleted automatically)
ND2.clear();
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Case 2: destroying a smart pointer
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// creating a NodeDummy (now reference counter = 1)
NodeDummyPtr ND1 = NodeDummy::create();
// setting the second pointer to point to the created NodeDummy (now reference counter = 2)
NodeDummyPtr ND2 = ND1;
// destroying ND1 pointer (the object is deleted automatically)
ND1.destroy();
// ND2 is no longer accessible, so this line will lead to an error
ND2->setEnabled(1);
Dealing with Hierarchies#
There is one thing about the destroy() method, that you should be aware of: you can load a large hierarchy of objects via World::get()->loadNode(), but when you delete a smart pointer of the root node of the hierarchy (e.g. by calling the destroy() method), only the node pointed by the interface will be deleted. All its children will become orphans and will remain in the world.
In case of a NodeReference things are simple: as you delete it, the whole its hierachy is deleted as well, so there's nothing to worry about. In all other cases we'll have to delete all children recursively. Therefore, at run time it is recommended either to create NodeReferences or single nodes via the World's loadNode() method.
Moreover, you can pass node ownership to the Editor (it always exists, even when you don't see it) and use the following:
- Editor::get()->addNode() - now the Editor will own the object and manage it. Don't forget to call release() for all interfaces before adding nodes to the Editor.
- Editor::get()->releaseNode() - release Editor ownership. Now the object can be controlled by you - just call grab() and it's yours.
- Editor::get()->removeNode() - deletes the node. The best thing in this case is that when the Editor is the owner it deletes the node with all its hierarchy automatically.
Generally, if you need to manage (create and delete) complex node hierarchies and change worlds, the best option might be to pass ownership to the Editor, after previously calling release() for all interfaces.
// creating a player and passing it to the Editor
PlayerSpectatorPtr spectator = PlayerSpectator::create();
spectator->release();
Editor::get()->addNode(spectator->getNode());
// loading a node from a file and passing it to the Editor
NodePtr node = World::get()->loadNode("my_node.node");
node->release();
Editor::get()->addNode(node);
// deleting a node with all its hierarchy
Editor::get()->removeNode(node);