I l@ve RuBoard Previous Section Next Section

6.6 Addressing the Dead Reference Problem (I):
The Phoenix Singleton

If we apply the solution in the previous section to the KDL (Keyboard, Display, Log) problem, the result is unsatisfactory. If Display's destructor needs to report an error after Log has been destroyed, Log::Instance throws an exception. We got rid of undefined behavior; now we have to deal with unsatisfactory behavior.

We need Log to be available at any time, no matter when it was initially constructed. At an extreme, we'd even prefer to create Log again (although it's been destroyed) so that we can use it for error reporting at any time. This is the idea behind the Phoenix Singleton design pattern.

Just as the legendary Phoenix bird rises repeatedly from its own ashes, the Phoenix singleton is able to rise again after it has been destroyed. A single instance of the Singleton object at any given moment is still guaranteed (no two singletons ever exist simultaneously), yet the instance can be created again if the dead reference is detected. With the Phoenix Singleton pattern, we can solve the KDL problem easily: Keyboard and Display can remain "regular" singletons, while Log becomes a Phoenix singleton.

The implementation of the Phoenix Singleton with a static variable is simple. When we detect the dead reference, we create a new Singleton object in the shell of the old one. (C++ guarantees this is possible. Static objects' memory lasts for the duration of the program.) We also register this new object's destruction with atexit. We don't have to change Instance; the only change is in the OnDeadReference primitive.



class Singleton


{


   ... as before ...


   void KillPhoenixSingleton(); // Added


};





void Singleton::OnDeadReference()


{


   // Obtain the shell of the destroyed singleton


   Create();


   // Now pInstance_ points to the "ashes" of the singleton


   // - the raw memory that the singleton was seated in.


   // Create a new singleton at that address


   new(pInstance_) Singleton;


   // Queue this new object's destruction


   atexit(KillPhoenixSingleton);


   // Reset destroyed_ because we're back in business


   destroyed_ = false;


}





void Singleton::KillPhoenixSingleton()


{


   // Make all ashes again


   // - call the destructor by hand.


   // It will set pInstance_ to zero and destroyed_ to true


   pInstance_->~Singleton();


}


The new operator that OnDeadReference uses is called the placement new operator. The placement new operator does not allocate memory; it only constructs a new object at the address passed—in our case, pInstance_. For an interesting discussion about the placement new operator, you may want to consult Meyers (1998b).

The Singleton above added a new member function, KillPhoenixSingleton. Now that we use new to resurrect the Phoenix singleton, the compiler magic will no longer destroy it, as it does with static variables. We built it by hand, so we must destroy it by hand, which is what atexit(KillPhoenixSingleton) ensures.

Let's analyze the flow of events. During the application exit sequence, Singleton's destructor is called. The destructor resets the pointer to zero and sets destroyed_ to true. Now assume some global object tries to access Singleton again. Instance calls OnDeadReference. OnDeadReference reanimates Singleton and queues a call to KillPhoenixSingleton, and Instance successfully returns a reference to a valid Singleton object. From now on, the cycle may repeat.

The Phoenix Singleton class ensures that global objects and other singletons can get a valid instance of it, at any time. This is a strong enough guarantee to make the Phoenix singleton an appealing solution for robust, all-terrain objects, like the Log in our problem. If we make Log a Phoenix singleton, the program will work correctly, no matter what the sequence of failures is.

6.6.1 Problems with atexit

If you compared the code given in the previous section with Loki's actual code, you would notice a difference: The call to atexit is surrounded by an #ifdef preprocessor directive:



#ifdef ATEXIT_FIXED


   // Queue this new object's destructor


   atexit(KillPhoenixSingleton);


#endif


If you don't #define ATEXIT_FIXED, the newly created Phoenix singleton will not be destroyed, and it will leak, which is exactly what we are striving to avoid.

This measure has to do with an unfortunate omission in the C++ standard. The standard fails to describe what happens when you register functions with atexit during a call made as the effect of another atexit registration.

To illustrate this problem, let's write a short test program:



#include <cstdlib>


void Bar()


{


   ...


}


void Foo()


{


   std::atexit(Bar);


}


int main()


{


   std::atexit(Foo);


}


This little program registers Foo with atexit. Foo calls atexit(Bar), a case in which the behavior is not covered by the two standards. Because atexit and destruction of static variables go hand in hand, once the application exit sequence has started, we're left on shaky ground. Both the C and C++ standards are self-contradictory. They say that Bar will be called before Foo because Bar was registered last, but at the time it's registered, it's too late for Bar to be first because Foo is already being called.

Does this problem sound too academic? Let's put it another way: At the time of this writing, on three widely used compilers, the behavior ranges from incorrect (resource leaks) to application crashes.[2]

[2] I have had a newsgroup (comp.std.c++) and e-mail discussion with Steve Clamage, chairman of the ANSI/ISO C++ Standards Committee, regarding the state of affairs with atexit. He was well aware of the problem and had already submitted a defect report for both C9X and C++. The submittal can be found at http://anubis.dkuug.dk/jtc1/sc22/wg21/docs/lwg-issues.html#3. Fortunately, the current proposed resolution favors the Singleton implementation discussed in this chapter: Even for functions called during exit processing, atexit still behaves in a stack manner, which is what is wanted. At the time of this writing, the resolution is approved and ready to be committed to the standard.

Compilers will take some time to catch up with the solution to this problem. As of this moment, the macro is in there. On some compilers, if you create a Phoenix singleton the second time, it will eventually leak. Depending on what your compiler's documentation says, you may want to #define ATEXIT_FIXED prior to including the Singleton.h header file.

    I l@ve RuBoard Previous Section Next Section