Boost.Singularity (Proposed)

Ben Robinson, Ph.D.

Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)

Table of Contents

Introduction
Motivation
Tutorial
Usage as a Factory
Usage as a Base Class
Enabling Global Access
Enabling Thread Safety
Limitations
Customization
Threading Policy
References

Introduction

The Singularity Design Pattern allows you to restrict any class to a single instance. Singularity gives you direct control over the lifetime of the object, and does not require you to grant global access to the object, nor limit you to a single constructor for that object.

Motivation

The ability to restrict a class to a single instance is an important technique. While the commonly used Singleton Design Pattern achieves the desired goal, it introduces three undesirable limitations, namely:

  1. Lifetime: Singleton introduces lifetime management complexities. The undefined destruction order of multiple singletons is just one example.
  2. Scope: Singletons are always globally accessible. Accessibility should be orthogonal to restrictions on the number of instances.
  3. Initialization: Singletons typically restricts the class to the default constructor. This makes Singleton difficult for classes which require non-default constructors.

Like Singleton, Singularity restricts a class to a single instance. However Singularity does not suffer from lifetime, scoping and initialization problems. The following are the advantages of Singularity over Singleton:

  1. The lifetime of the single object is bounded by calls to create() and destroy(). This lifetime is managed similiarly to objects bounded by calls to new() and delete().
  2. Singularity does not force global access to the instance. If global access is required, then a globally accessible get_global() function can be enabled upon creation of the instance.
  3. Singularities of objects can be created using any available constructor for that object.

Tutorial

To use Singularity, either as a factory or a base class, the class which is to be singular must make all constructors private, and friend the singularity class. A macro is provided to simplify the friend statement:

#define FRIEND_CLASS_SINGULARITY \
    template <class T, template <class T> class M> friend class singularity

Usage as a Factory

Usage as a factory is shown as follows:

#include <singularity.hpp>

using ::boost::singularity;

// This is the class which will be singularized.
// Notice the private constructors and friend statement.
class Horizon
{
private:
    Horizon() {}
    Horizon(int arg0) {}
    ...
    FRIEND_CLASS_SINGULARITY;
};

int main()
{
    // Create using the default constructor.
    Horizon & firstHorizon = singularity<Horizon>::create();
    // Use the firstHorizon here...
    singularity<Horizon>::destroy();
	
    // Create using a non-default constructor.
    Horizon & secondHorizon = singularity<Horizon>::create(7);
    // Use the secondHorizon here...
    singularity<Horizon>::destroy();

    return 0;
}

Usage as a Base Class

Usage as a base class is shown as follows:

#include <singularity.hpp>

using ::boost::singularity;

// This is the class which will be singularized.
// Notice the private constructors and friend statement.
// Also notice the curiously recurring template pattern.
class Horizon : public singularity<Horizon>
{
private:
    Horizon() {}
    Horizon(int arg0) {}
    ...
    FRIEND_CLASS_SINGULARITY;
};

int main()
{
    // Create using the default constructor.
    Horizon & firstHorizon = Horizon::create();
    // Use the firstHorizon here...
    Horizon::destroy();
	
    // Create using a non-default constructor.
    Horizon & secondHorizon = Horizon::create(7);
    // Use the secondHorizon here...
    Horizon::destroy();

    return 0;
}

Enabling Global Access

To enable global access to the instance through the get_global() member function, use create_global() instead of create() when instantiating the object. An example is shown:

#include <singularity.hpp>

using ::boost::singularity;

class Horizon
{
private:
    Horizon() {}
    Horizon(int arg0) {}
    ...
    FRIEND_CLASS_SINGULARITY;
};

int main()
{
    // Create using the default constructor.
    Horizon & horizon = singularity<Horizon>::create_global();
    {
        // Someplace far away in the code.
        Horizon & sameHorizon = singularity<Horizon>::get_global();
        // Use the sameHorizon here...
    }
    singularity<Horizon>::destroy();

    return 0;
}

Enabling Thread Safety

To enable thread safety, supply a second template argument to singularity as "multi_threaded". An example is shown below:

#include <singularity.hpp>

using ::boost::singularity;
using ::boost::multi_threaded;

class Horizon
{
private:
    Horizon() {}
    Horizon(int arg0) {}
    ...
    FRIEND_CLASS_SINGULARITY;
};

int main()
{
    // Create using the default constructor.
    Horizon & horizon = singularity<Horizon, multi_threaded>::create_global();
    {
        // Someplace far away in the code.
        Horizon & sameHorizon = singularity<Horizon, multi_threaded>::get_global();
        // Use the sameHorizon here...
    }
    singularity<Horizon, multi_threaded>::destroy();

    return 0;
}

To use a thread safe singularity as a base class, inherit from singularity as shown below:

class Horizon : public singularity<Horizon, multi_threaded>

Because the Double-Checked Locking Pattern is not both thread-safe and portable (see Reference 2), the multi_threaded policy mutex is always acquired when calling on any member function of singularity. When using the singularity with create_global(), due to the performance impact of acquiring a mutex, it is recommended that get_global() be called infrequently, and the returned reference stored for later use.

Limitations

As a generic wrapper class, singularity needs to perfectly forward the arguments passed from the create() function, into the singularized class. Unfortunately with C++03, this is not possible for an arbitrary number of arguments, without an O(2^n) implementation. This is known as the forwarding problem.

Just like to Boost.Bind, most create() functions must receive their arguments as non-const references. However, for functions requiring up to 3 arguments, all arguments will be perfectly forwarded. The number of required create() functions is O(2^n), 2*(2^(n+1)-1) to be exact, so the default upper limit of 3 arguments requires generating 30 functions. Constructors which take additional arguments receive them as non-const references up to 10 arguments by default.

#ifndef BOOST_SINGULARITY_PERFECT_FORWARD_ARG_SIZE
#define BOOST_SINGULARITY_PERFECT_FORWARD_ARG_SIZE 3
#endif

#ifndef BOOST_SINGULARITY_NONCONST_REFERENCE_ARG_SIZE
#define BOOST_SINGULARITY_NONCONST_REFERENCE_ARG_SIZE 10
#endif

These arbitrary values can be redefined by the user before including the header, as appropriate for their needs. The specified number of forwarding create(...) functions will be automatically generated as required.

Customization

Threading Policy

The supplied thread safe policy of multi_threaded is defined as follows:

template <class T> class multi_threaded
{
public:
    inline multi_threaded()
    {
        lockable.lock();
    }
    inline ~multi_threaded()
    {
        lockable.unlock();
    }
private:
    static ::boost::mutex lockable;
};

Instantiation of this object creates an RAII style lock protecting access to the code in scope. Developers on small microcontrollers which do not support exceptions, will be unable to use this policy object, as the boost::mutex requires exceptions to be enabled. If for this, or any other reason, the developer is unable to use the supplied multi_threaded policy, an alternate policy can be implemented and supplied to singularity. The new policy need only acquire a mutex on construction, and release it upon destruction.

References

  1. The Forwarding Problem, Peter Dimov, Howard E. Hinnant, Dave Abrahams, 2002
  2. C++ and the Perils of Double-Checked Locking, Scott Meyers and Andrei Alexandrescu, 2004
  3. Volatile: Almost Useless for Multi-Threaded Programming, Arch Robison, 2007
  4. Design Patterns, Gamma et al. - Addison Wesley Publishing, 1995
  5. Modern C++ Design, Andrei Alexandrescu, 2001
  6. Boost.Bind, Peter Dimov, 2001-2005