ADTF
Reference counting provided by the uCOM

Introduction to uCOM reference counting

The uCOM offers some functionality to share ownership of objects through pointers. For this purpose, the STL offers several so called smart pointers, e.g. std::shared_ptr, std::weak_ptr or std::unique_ptr. However, these smart pointers do have the major drawback that they cannot be used to share resources between binary boundaries. E.g. compiling an STL smart pointer in release mode leads to an entire different object layout than in debug mode. To realize a shared ownership even between binary boundaries, the uCOM offers an own reference counting mechanism which is implemented with object_ptr and iobject_ptr.

To use reference counting on objects through pointers with object_ptr and iobject_ptr, some things must be kept in mind:

  1. Reference counting is only provided on objects of type IObject
  2. To create shared objects, make_object_ptr() must be used
  3. Conversions of managed objects may only be performed on types that can be casted via ucom_cast()

Usage of the uCOM reference counting

Given the following inheritance hierarchy (taken from The ucom_cast<> in depth explanation):

class IEnvironmentalBurden
{
public:
ADTF_IID(IEnvironmentalBurden, "environmental_burden.adtf.example.iid");
virtual ~IEnvironmentalBurden() = default;
public: //pure virtual functions
//returns the emission class of this object as character.
//possible characters are: 'A', 'B', 'C', 'D', 'E', 'F'
virtual char EmissionClass() const = 0;
};
#define ADTF_IID(_interface, _striid)
Common macro to enable correct treatment of interface classes by the adtf::ucom::ucom_cast<>
Definition: adtf_iid.h:17
class IStatusSymbol
{
public:
ADTF_IID(IStatusSymbol, "status_symbol.adtf.example.iid");
virtual ~IStatusSymbol() = default;
public: //pure virtual functions
//rating of this status symbol from 0 being the lowest rating to 10 being the highest rating
virtual uint32_t Rating() const = 0;
//every status symbol has a price
virtual float Price() const = 0;
};
//vehicles are objects and an environmental burden
class IVehicle : public adtf::ucom::IObject, public IEnvironmentalBurden
{
public:
ADTF_IID(IVehicle, "vehicle.adtf.example.iid");
virtual ~IVehicle() = default;
public: //pure virtual functions
virtual const char* Name() const = 0; //retrieves the name of the car
virtual bool UrbanApproved() const = 0; //indicates whether the car is urban approved
};
Base class for every interface type within the uCOM.
Definition: object_intf.h:31
//a car is a vehicle and a status symbol
class cCar : public IVehicle, public IStatusSymbol
{
private:
//we can alias the exposed interfaces anywhere in the code and use those in GetInterface()
IEnvironmentalBurden,
IStatusSymbol,
IVehicle> expose;
public:
//construct car with
cCar(char ui8EmissionClass,
uint32_t ui32Rating,
float f32Price,
const char* strColor,
bool bUrbanApproved);
//destructor
virtual ~cCar();
public: //implementing the interfaces
char EmissionClass() const;
uint32_t Rating() const;
float Price() const;
const char* Name() const;
bool UrbanApproved() const;
private: //IObject implementation
virtual void Destroy() const;
virtual tResult GetInterface(const char* strIID, void*& o_pInterface);
virtual tResult GetInterface(const char* strIID, const void*& o_pInterface) const;
private: //member section
const char m_ui8EmissionClass; //emission class
const uint32_t m_ui32Rating; //status symbol rating
const float m_f32Price; //price
const char* const m_strName; //name of the car
const bool m_bUrbanApproved; //whether it's urban approved
};
Meta template struct used to expose all interfaces.
Definition: adtf_iid.h:467
ant::IObject IObject
Alias always bringing the latest version of ant::IObject into scope.
Definition: object_intf.h:102

Instantiating reference counted objects

To create an object_ptr managing the ownership of an object cCar through a pointer of IVehicle one would have to use make_object_ptr().

//parameters of the cars to add: 'EnvBurden', 'Rating', 'Price', 'Name', 'Urban'
object_ptr<IVehicle> pVehicle = make_object_ptr<cCar>('A', 2, 12900.00, "Medium Car", tTrue);
#define tTrue
Value for tBool.
Definition: constants.h:62

Of course, const correctness is provided:

//parameters of the cars to add: 'EnvBurden', 'Rating', 'Price', 'Name', 'Urban'
object_ptr<const IVehicle> pVehicle = make_object_ptr<const cCar>('A', 2, 12900.00, "Medium Car", tTrue);

make_object_ptr() is intended to work exactly like std::make_shared(). The parameters are directly forwarded to the constructor of cCar during construction. No 'traditional' memory allocation is needed by the user, so no new or malloc must be called. In fact, make_object_ptr() is the only way to instantiate a new reference counted object. make_object_ptr() returns the allocated object with a reference count of 1.

object_ptr offers several constructors and assignment operators. This makes it possible to 'convert' one object_ptr managing type A to another one managing type B - as long as type A is castable to type B with ucom_cast(). In this example the managed resource cCar gets casted to IVehicle in the aliasing constructor of object_ptr. This makes the instantiation of object_ptr instances managing objects through interface pointers very convenient.

Lifetime of reference counted objects

With the reference counting wrapped into object_ptr, the developer doesn't need to take care of reference counting at all. Copying, moving, deleting and creating object_ptr instances entirely takes care of the lifetime of the managed object under the hood. Just like std::shared_ptr objects, the managed object is destroyed and its memory deallocated when either of the following happens:

The managed object is destructed and deallocated using the supplied IObject::Destroy() method. In fact, the object_ptr is the only class having access to this method, restricting object (de-)allocation of type IObject to just make_object_ptr and object_ptr.

Binary compatible reference counting

To ensure binary compatible reference counting, pure abstract base class iobject_ptr is introduced. If a reference counting is required in interface methods, this class is the parameter type to use. object_ptr being derived from iobject_ptr ensures implicit upcasting and convenient usage on calling functions. object_ptr also implements a conversion operator to iobject_ptr<IObject> or iobject_ptr<const IObject>, depending on the constness of the managed object type (object_ptr_base).

Thus, code like this is totally valid:

//parameters of the cars to add: 'EnvBurden', 'Rating', 'Price', 'Name', 'Urban'
object_ptr<IVehicle> pVehicle = make_object_ptr<cCar>('A', 2, 12900.00, "Medium Car", tTrue);
//create reference to base class iobject<IVehicle>, which is only an implicit upcast
iobject<IVehicle>& pVehicleRef = pVehicle;
//create reference to iobject<IObject>, which calls the conversion operator
iobject<IObject>& pVehicleObjectRef = pVehicle;

Using this mechanism in functions or methods ensures an entirely binary compatible reference counting. The following code shows an example how this might be used:

void funcFoobar(iobject_ptr<IVehicle>& io_pVehicle)
{
//parameters of the cars to add: 'EnvBurden', 'Rating', 'Price', 'Name', 'Urban'
object_ptr<IVehicle> pVehicle = make_object_ptr<cCar>('A', 2, 12900.00, "Medium Car", tTrue);
//'assign' the output parameter with iobject_ptr::Reset()
io_pVehicle.Reset(pVehicle);
}
//[...]
//call funcFoobar
object_ptr<IVehicle> pVehicle; //gets 'assigned' inside funcFoobar()
funcFoobar(pVehicle); //pVehicle gets implicitly converted to iobject_ptr<IVehicle>&

Limitations of the object_ptr

Main purposes of object_ptr and iobject_ptr are to

  1. realize reference counting of objects that are transfered between binary boundaries and
  2. to enable safe type conversions between objects without losing track of their lifetime

Despite the fact, that it might be used as a more generalized reference counting mechanism, these requirements limit the usage of object_ptr and iobject_ptr to objects of types derived from IObject. To fulfill the first requirement, object_ptr calls the IObject::Destroy() method to ensure the deallocator is called in the same DLL as the object was allocated. To fulfill the second requirement, object_ptr uses the ucom_cast() which also only works on types derived from IObject.

Thus, code like this won't compile:

//parameters of the cars to add: 'EnvBurden', 'Rating', 'Price', 'Name', 'Urban'
object_ptr<IStatusSymbol> pStatusSymbol = make_object_ptr<cCar>('A', 2, 12900.00, "Medium Car", tTrue);

IStatusSymbol can not be used as managed resource as it is not derived from IObject.