Copyright © 2011 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
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.
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:
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:
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 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 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; }
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; }
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.
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.
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.