Medical Imaging Interaction Toolkit  2024.06.00
Medical Imaging Interaction Toolkit
Emulating singletons with micro services

Meyers Singleton

Singletons are a well known pattern to ensure that only one instance of a class exists during the whole life-time of the application. A self-deleting variant is the "Meyers Singleton":

class SingletonOne
{
public:
static SingletonOne& GetInstance();
// Just some member
int a;
private:
SingletonOne();
~SingletonOne();
// Disable copy constructor and assignment operator.
SingletonOne(const SingletonOne&);
SingletonOne& operator=(const SingletonOne&);
};

where the GetInstance() method is implemented as

SingletonOne& SingletonOne::GetInstance()
{
static SingletonOne instance;
return instance;
}

If such a singleton is accessed during static deinitialization (which happens during unloading of shared libraries or application termination), your program might crash or even worse, exhibit undefined behavior, depending on your compiler and/or weekday. Such an access might happen in destructors of other objects with static life-time.

For example, suppose that SingletonOne needs to call a second Meyers singleton during destruction:

SingletonOne::~SingletonOne()
{
std::cout << "SingletonTwo::b = " << SingletonTwo::GetInstance().b << std::endl;
}

If SingletonTwo was destroyed before SingletonOne, this leads to the mentioned problems. Note that this problem only occurs for static objects defined in the same shared library.

Since you cannot reliably control the destruction order of global static objects, you must not introduce dependencies between them during static deinitialization. This is one reason why one should consider an alternative approach to singletons (unless you can absolutely make sure that nothing in your shared library will introduce such dependencies. Never.)

Of course you could use something like a "Phoenix singleton" but that will have other drawbacks in certain scenarios. Returning pointers instead of references in GetInstance() would open up the possibility to return NULL, but than again this would not help if you require a non-NULL instance in your destructor.

Another reason for an alternative approach is that singletons are usually not meant to be singletons for eternity. If your design evolves, you might hit a point where you suddenly need multiple instances of your singleton.

Singletons as a service

The C++ Micro Services can be used to emulate the singleton pattern using a non-singleton class. This leaves room for future extensions without the need for heavy refactoring. Additionally, it gives you full control about the construction and destruction order of your "singletons" inside your shared library or executable, making it possible to have dependencies between them during destruction.

Converting a classic singleton

We modify the previous SingletonOne class such that it internally uses the micro services API. The changes are discussed in detail below.

class SingletonOneService
{
public:
// This will return a SingletonOneService instance with the
// lowest service id at the time this method was called the first
// time and returned a non-null value (which is usually the instance
// which was registered first). A null-pointer is returned if no
// instance was registered yet.
static SingletonOneService* GetInstance();
int a;
private:
// Only our module activator class should be able to instantiate
// a SingletonOneService object.
friend class MyActivator;
SingletonOneService();
~SingletonOneService();
// Disable copy constructor and assignment operator.
SingletonOneService(const SingletonOneService&);
SingletonOneService& operator=(const SingletonOneService&);
};
  • In the implementation above, the class SingletonOneService provides the implementation as well as the interface.
  • Friend activator: We move the responsibility of constructing instances of SingletonOneService from the GetInstance() method to the module activator.

Let's have a look at the modified GetInstance() and ~SingletonOneService() methods.

SingletonOneService* SingletonOneService::GetInstance()
{
static ServiceReference<SingletonOneService> serviceRef;
static ModuleContext* context = GetModuleContext();
if (!serviceRef)
{
// This is either the first time GetInstance() was called,
// or a SingletonOneService instance has not yet been registered.
serviceRef = context->GetServiceReference<SingletonOneService>();
}
if (serviceRef)
{
// We have a valid service reference. It always points to the service
// with the lowest id (usually the one which was registered first).
// This still might return a null pointer, if all SingletonOneService
// instances have been unregistered (during unloading of the library,
// for example).
return context->GetService(serviceRef);
}
else
{
// No SingletonOneService instance was registered yet.
return nullptr;
}
}

The inline comments should explain the details. Note that we now had to change the return type to a pointer, instead of a reference as in the classic singleton. This is necessary since we can no longer guarantee that an instance always exists. Clients of the GetInstance() method must check for null pointers and react appropriately.

Warning
Newly created "singletons" should not expose a GetInstance() method. They should be handled as proper services and hence should be retrieved by clients using the ModuleContext or ServiceTracker API. The GetInstance() method is for migration purposes only.
SingletonOneService::~SingletonOneService()
{
SingletonTwoService* singletonTwoService = SingletonTwoService::GetInstance();
// The module activator must ensure that a SingletonTwoService instance is
// available during destruction of a SingletonOneService instance.
assert(singletonTwoService != nullptr);
std::cout << "SingletonTwoService::b = " << singletonTwoService->b << std::endl;
}

The SingletonTwoService::GetInstance() method is implemented exactly as in SingletonOneService. Because we know that the module activator guarantees that a SingletonTwoService instance will always be available during the life-time of a SingletonOneService instance (see below), we can assert a non-null pointer. Otherwise, we would have to handle the null-pointer case.

The order of construction/registration and destruction/unregistration of our singletons (or any other services) is defined in the Load() and Unload() methods of the module activator.

void Load(ModuleContext* context) override
{
// The Load() method of the module activator is called during static
// initialization time of the shared library.
// First create and register a SingletonTwoService instance.
m_SingletonTwo = new SingletonTwoService;
m_SingletonTwoReg = context->RegisterService<SingletonTwoService>(m_SingletonTwo);
// Now the SingletonOneService constructor will get a valid
// SingletonTwoService instance.
m_SingletonOne = new SingletonOneService;
m_SingletonOneReg = context->RegisterService<SingletonOneService>(m_SingletonOne);
}

The Unload() method is defined as:

void Unload(ModuleContext* /*context*/) override
{
// Services are automatically unregistered during unloading of
// the shared library after the call to Unload(ModuleContext*)
// has returned.
// Since SingletonOneService needs a non-null SingletonTwoService
// instance in its destructor, we explicitly unregister and delete the
// SingletonOneService instance here. This way, the SingletonOneService
// destructor will still get a valid SingletonTwoService instance.
m_SingletonOneReg.Unregister();
delete m_SingletonOne;
// For singletonTwoService, we could rely on the automatic unregistering
// by the service registry and on automatic deletion if you used
// smart pointer reference counting. You must not delete service instances
// in this method without unregistering them first.
m_SingletonTwoReg.Unregister();
delete m_SingletonTwo;
}
ModuleContext::GetServiceReference
ServiceReferenceU GetServiceReference(const std::string &clazz)
ModuleContext
Definition: usModuleContext.h:91
us::GetModuleContext
static ModuleContext * GetModuleContext()
Returns the module context of the calling module.
Definition: usGetModuleContext.h:50
ModuleContext::GetService
void * GetService(const ServiceReferenceBase &reference)
ModuleContext::RegisterService
ServiceRegistrationU RegisterService(const InterfaceMap &service, const ServiceProperties &properties=ServiceProperties())