I l@ve RuBoard Previous Section Next Section

6.8 Implementing Singletons with Longevity

Once SetLongevity's specification is complete, the implementation is not that complicated. SetLongevity maintains a hidden priority queue, separate from the inaccessible atexit stack. In turn, SetLongevity calls atexit, always passing it the same pointer to a function. This function pops one element off the stack and deletes it. It's that simple.

The core is the priority queue data structure. The longevity value passed to SetLongevity establishes the priorities. For a given longevity, the queue behaves like a stack. Destruction of objects with the same longevity follows the last in, first out rule. In spite of its name, we cannot use the standard std::priority_queue class because it does not guarantee the ordering of the elements having the same priority.

The elements that the data structure holds are pointers to the type LifetimeTracker. Its interface consists of a virtual destructor and a comparison operator. Derived classes must override the destructor. (You will see in a minute what Compare is good for.)



namespace Private


{


   class LifetimeTracker


   {


   public:


      LifetimeTracker(unsigned int x) : longevity_(x) {}


      virtual ~LifetimeTracker() = 0;


      friend inline bool Compare(


         unsigned int longevity,


         const LifetimeTracker* p)


      { return p->longevity_ > longevity; }


   private:


      unsigned int longevity_;


   };


   // Definition required


   inline LifetimeTracker::~LifetimeTracker() {}


}


The priority queue data structure is a simple dynamically allocated array of pointers to LifetimeTracker:



namespace Private


{


   typedef LifetimeTracker** TrackerArray;


   extern TrackerArray pTrackerArray;


   extern unsigned int elements;


}


There is only one instance of the Tracker type. Consequently, pTrackerArray is exposed to all the Singleton problems just discussed. We are caught in an interesting chicken-and-egg problem: SetLongevity must be available at any time, yet it has to manage private storage. To deal with this problem, SetLongevity carefully manipulates pTrackerArray with the low-level functions in the std::malloc family (malloc, realloc, and free).[3] This way, we transfer the chicken-and-egg problem to the C heap allocator, which fortunately is guaranteed to work correctly for the whole lifetime of the application. This being said, SetLongevity's implementation is simple: It creates a concrete tracker object, adds it to the stack, and registers a call to atexit.

[3] Actually, SetLongevity uses std::realloc only. The realloc function can replace both malloc and free: If you call it with a null pointer, it behaves like std::malloc; if you call it with a zero size, it behaves like std::free. Basically, std::realloc is a one-stop shop for malloc-based allocation.

The following code makes an important step toward generalization. It introduces a functor that takes care of destroying the tracked object. The rationale is that you don't always use delete to deallocate an object; it can be an object allocated on an alternate heap, and so on. By default, the destroyer is a pointer to a function that calls delete. The default function is called Delete and is templated with the type of the object to be deleted.



//Helper destroyer function


template <typename T>


struct Deleter


{


   static void Delete(T* pObj)


   { delete pObj; }


};





// Concrete lifetime tracker for objects of type T


template <typename T, typename Destroyer>


class ConcreteLifetimeTracker : public LifetimeTracker


{


public:


      ConcreteLifetimeTracker(T* p,


         unsigned int longevity,


         Destroyer d)


         :LifetimeTracker(longevity),


      ,pTracked_(p)


      ,destroyer_(d)


      {}


      ~ConcreteLifetimeTracker()


      {


         destroyer_(pTracked_);


      }


   private:


      T* pTracked_;


      Destroyer destroyer_;


   };


   void AtExitFn(); // Declaration needed below


}





template <typename T, typename Destroyer>


void SetLongevity(T* pDynObject, unsigned int longevity,


   Destroyer d = Private::Deleter<T>::Delete)


{


   TrackerArray pNewArray = static_cast<TrackerArray>(


           std::realloc(pTrackerArray, sizeof(T) * (elements + 1)));


   if (!pNewArray) throw std::bad_alloc();


   pTrackerArray = pNewArray;


   LifetimeTracker* p = new ConcreteLifetimeTracker<T, Destroyer>(


       pDynObject, longevity, d);


   TrackerArray pos = std::upper_bound(


      pTrackerArray, pTrackerArray + elements, longevity, Compare);


   std::copy_backward(pos, pTrackerArray + elements,


      pTrackerArray + elements + 1);


   *pos = p;


   ++elements;


   std::atexit(AtExitFn);


}


It takes a while to get used to things like std::upper_bound and std::copy_backward, but indeed they make nontrivial code easy to write and read. The function above inserts a newly created pointer to ConcreteLifetimeTracker in the sorted array pointed to by pTrackerArray, keeps it ordered, and handles errors and exceptions.

Now the purpose of LifetimeTracker::Compare is clear. The array to which pTrackerQueue points is sorted by longevity. Objects with longer longevity are toward the beginning of the array. Objects of the same longevity appear in the order in which they were inserted. SetLongevity ensures all this.

The AtExitFn function pops the object with the smallest longevity (that is, the one at the end of the array) and deletes it. Deleting the pointer to LifetimeTracker invokes ConcreteLifetimeTracker's destructor, which in turn deletes the tracked object.



static void AtExitFn()


{


   assert(elements > 0 && pTrackerArray != 0);


   // Pick the element at the top of the stack


   LifetimeTracker* pTop = pTrackerArray[elements - 1];


   // Remove that object off the stack


   // Don't check errors-realloc with less memory


   // can't fail


   pTrackerArray = static_cast<TrackerArray>(std::realloc(


       pTrackerArray, sizeof(T) * --elements));


   // Destroy the element


   delete pTop;


}


Writing AtExitFn requires a bit of care. AtExitFn must pop the top element off the stack and delete it. In its destructor, the element deletes the managed object. The trick is, AtExitFn must pop the stack before deleting the top object because destroying some object may create another one, thus pushing another element onto the stack. Although this looks quite unlikely, it's exactly what happens when Keyboard's destructor tries to use the Log.

The code conveniently hides the data structures and AtExitFn in the Private namespace. The clients see only the tip of the iceberg—the SetLongevity function.

Singletons with longevity can use SetLongevity in the following manner:



class Log


{


public:


   static void Create()


   {


      // Create the instance


      pInstance_ = new Log;


      // This line added


      SetLongevity(*this, longevity_);


   }


   // Rest of implementation omitted


   // Log::Instance remains as defined earlier


private:


   // Define a fixed value for the longevity


   static const unsigned int longevity_ = 2;


   static Log* pInstance_;


};


If you implement Keyboard and Display in a similar fashion, but define longevity_ to be 1, the Log will be guaranteed to be alive at the time when both Keyboard and Display are destroyed. This solves the KDL problem—or does it? What if your application uses multiple threads?

    I l@ve RuBoard Previous Section Next Section