Nebula Device' smart pointer implementation

Feb 2, 2022 · 5 mins read

Nebula Device' smart pointer implementation

Today I will take a look at how reference counted smart pointers were implemented in Nebula Device.

Nebula Device

Nebula Device is an open source 3D game engine developed by (now defunct) Randon Labs GmbH, the company behind one of my old favourite games “Drakensang: Das Schwarze Auge”. The latest version of this engine was uploaded to Google Code in 2011, and it seems like development has stopped since then. Chinese Changyou later created a fork of Nebula Device called Genesis3D where they added various new features (such as C# scripting), but this project has apparently also stagnated.

Enough history! Let’s look at their smart pointer implementation.

Reference counted smart pointers

Since “garbage collection” is not a concept in C++, we are expected to clean up our own garbage. When dynamically allocating data (using new) we need to free it (using delete) when it’s no longer in use to avoid memory leaks. The standard library has (since C++11) some useful smart pointer templates that we can use, such as shared_ptr. While this is a great general purpose smart pointer, you can also create your own implementation, depending on what your requirements are (whether you should is another discussion).

Smart pointers will typically store a reference count to an object. When a new object needs to reference it (by instantiating a smart pointer), the reference count is increased. When the reference is no longer needed (when the smart pointer is released) the reference count will be decremented, and if the reference count reaches zero (meaning: the object is no longer referenced) the object will automatically be destroyed.

So where do we store the reference count? There are several alternatives. We could store it in an int pointer (or other type) that is shared between all smart pointers pointing to the same object. All smart pointers pointing to the same object will then also have the same reference count. However, another alternative would be to store it in a RefCounted class that the object is required to inherit from. This is not a good alternative for a general purpose smart pointer library (since it will only work for classes that inherit from RefCounted), but it can be a good solution for managing objects in a game or game engine, where these objects already have a common base class. This is the approach used in Nebula Engine, and the advantage of this approach is:

  1. You can create smart pointers directly from the raw pointer to an object.
  2. You can force-destroy an object, if you want, by setting its reference count to zero, and all smart pointers will automatically become invalidated.

You might argue that (1) is not a big benefit and that (2) is not necessary if you properly use shared_ptr and weak_ptr to manage ownership of your objects, and I’m not going to argue against that. However, I still think it’s an interesting approach.

Ptr and RefCounted

All reference counted objects need to be of a class that inherits from the RefCounted class. The class looks like this:

namespace Core
{
class RefCounted
{
private:
    volatile int refCount;

public:
    /// constructor
    RefCounted();
    /// get the current refcount
    int GetRefCount() const;
    /// increment refcount by one
    void AddRef();
    /// decrement refcount and destroy object if refcount is zero
    void Release();
    /// .... omitted code ...

The refCount variable stores the reference count of the class instance.

The most interesting functions of the class are the AddRef and Release functions. As the name implies AddRef will add a reference to this object:

inline void
RefCounted::AddRef()
{
    Threading::Interlocked::Increment(this->refCount);
}

Release will remove a reference to the object, and destroy it if there are no more references to it.

inline void
RefCounted::Release()
{
    if (0 == Threading::Interlocked::Decrement(this->refCount))
    {
        n_delete(this);
    }
}

The smart pointer class is called Ptr and looks like this:

template<class TYPE>
class Ptr
{
public:
    /// constructor
    Ptr();
    /// construct from C++ pointer
    Ptr(TYPE* p);
    /// construct from smart pointer
    Ptr(const Ptr<TYPE>& p);
    /// destructor
    ~Ptr();
    /// assignment operator
    void operator=(const Ptr<TYPE>& rhs);
    /// assignment operator
    void operator=(TYPE* rhs);
    /// equality operator
    bool operator==(const Ptr<TYPE>& rhs) const;
    /// inequality operator
    bool operator!=(const Ptr<TYPE>& rhs) const;
    /// shortcut equality operator
    bool operator==(const TYPE* rhs) const;
    /// shortcut inequality operator
    bool operator!=(const TYPE* rhs) const;
    /// safe -> operator
    TYPE* operator->() const;
    /// safe dereference operator
    TYPE& operator*() const;
    /// safe pointer cast operator
    operator TYPE*() const;
    /// type-safe downcast ope

As you can see, it overrides quite a few operators, such as:

  • operator== for equality checks
  • operator* for dereferencing
  • operator= for assignment

It also has a few constructors. One of them constructs a Ptr from another Ptr:

template<class TYPE>
Ptr<TYPE>::Ptr(TYPE* p) :
    ptr(p)
{
    if (0 != this->ptr)
    {
        this->ptr->AddRef();
    }
}

This is done simply by storing the raw pointer to the object in the ptr variable, and calling RefCounted::AddRef to increment the reference count.

When the smart pointer goes out of scope, it will decrement the reference count. This is implemented in the destructor:

template<class TYPE>
Ptr<TYPE>::~Ptr()
{
    if (0 != this->ptr)
    {
        this->ptr->Release();
        this->ptr = 0;
    }
}

And that’s it! The smart pointers can be used like this:

Ptr<MyClass> ptr = new MyClass(); // assign from raw pointer

Sharing is caring!