Emulating Singletons

Integrating C++ Micro Services into an existing code-base can be done incrementally, e.g. by starting to convert class singletons to 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, 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 a nullptr, 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

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). An empty object is returned if no
  // instance was registered yet.
  //
  // Note: This is a helper method to migrate traditional singletons to
  // services. Do not create a method like this in real world applications.
  static std::shared_ptr<SingletonOneService> GetInstance();

  int a;
  
  SingletonOneService();
  ~SingletonOneService();

private:
  // 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.
  • The SingletonOneService class looks like a plain C++ class, no need for hiding constructors and destructor

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

std::shared_ptr<SingletonOneService> SingletonOneService::GetInstance()
{
  static ServiceReference<SingletonOneService> serviceRef;
  static auto context = GetBundleContext();

  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 shared 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 if the returned object is empty and react appropriately.

Note

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 BundleContext or ServiceTracker API. The GetInstance() method is for migration purposes only.

SingletonOneService::~SingletonOneService()
{
  std::shared_ptr<SingletonTwoService> singletonTwoService = SingletonTwoService::GetInstance();

  // The bundle 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 bundle 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 Start() and Stop() methods of the bundle activator.

  void Start(BundleContext context)
  {
    // First create and register a SingletonTwoService instance.
    m_SingletonTwo = std::make_shared<SingletonTwoService>();
    m_SingletonTwoReg = context.RegisterService<SingletonTwoService>(m_SingletonTwo);
    // Framework service registry has shared ownership of the SingletonTwoService instance

    // Now the SingletonOneService constructor will get a valid
    // SingletonTwoService instance.
    m_SingletonOne = std::make_shared<SingletonOneService>();
    m_SingletonOneReg = context.RegisterService<SingletonOneService>(m_SingletonOne);
  }

The Stop() method is defined as:

  void Stop(BundleContext /*context*/)
  {
    // Services are automatically unregistered during unloading of
    // the shared library after the call to Stop(BundleContext*)
    // 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();
    m_SingletonOne.reset();
    // Deletion of the SingletonTwoService instance is handled by the smart pointer

    // For singletonTwoService, we could rely on the automatic unregistering
    // by the service registry and on automatic deletion of service
    // instances through smart pointers.
    m_SingletonTwoReg.Unregister();
    m_SingletonTwo.reset();
    // Deletion of the SingletonOneService instance is handled by the smart pointer
  }