How to write maintainable initialisation and termination code

From rdiez's Personal Wiki
Jump to navigationJump to search

The traditional, C-style way is still the best one.

 int main ( int argc, char * argv[] )
  create some threads
  do some work
  wait for thread termination
  // Here you can check for memory and file descriptor leaks.
  return whatever;

Don't over-engineer it! The method above is not cool, but is perfectly adequate. It's easy to understand and to maintain. Sometimes it can be tricky to find the right initialisation order, but then it will be explicitly described, and above all, it will remain the same no matter what the other code does.

The only real drawback is that, if you have different applications/programs, you need to write a new version of the above for each one, and probably most of it will be copy-paste duplication. But that is a minor inconvenience.

Do not define global or static object instances, use pointers instead

Global or static object instances generate constructor calls that are executed before main(), and that may be too early, if not today, then probably in the future.

Besides, singleton objects tend to use other global, singleton objects, and the initialisation order of such objects is not guaranteed in the C standard. The order in which the constructors are called may depend on your operating system, on your compiler version, or even on the order in which the object modules are listed on your makefile (the order in the linker command line).

If you want to reuse the code in an embedded application, you may find it difficult to control the initialisation order, especially since it is often poorly documented or hard to adapt. For example, think about the PowerPC EABI, which injects a hidden __eabi() call in main() in order to initialise C++ exception support and so on. Say you want to use your own memory allocator and __eabi() is calling some C++ constructors before your code runs: you'll have to learn much more about EABI and about the C runtime initialisation than you ever wanted to.

If you need to move the code to a Windows DLL or to a Unix Shared Object at a later point in time, you will have to rewrite the initialisation logic anyway.

Therefore, always use global or static pointers, and let them be NULL on start-up, then follow the initialisation method described above:

 GetSingletonA()->UseIt();  GetSingletonB()->UseIt();  ...
 GetSingletonA()->UseIt();  GetSingletonB()->UseIt();  ...

Do not initialise global singletons on first touch

Unless you really need the performance improvement that lazy initialisation may provide, avoid using such an object creation pattern:

 MyClass * GetSingleton ( void )
   static MyClass obj;
   return &obj;

Reasons to avoid it are:

  • The function above may not be thread-safe on all platforms, especially on embedded targets.
  • The destructor will run after main(), making memory leak detection difficult.
    An error during destruction may need to be logged, but the logger object may have been destroyed already.
  • The destruction order for such objects is not specified, not portable, etc.
    At some point in the future, you may need to destroy some other singleton beforehand, and that's impossible to do in a portable way.
  • The initialisation order will depend on the code that uses the singletons.
    If the usage pattern changes, the object may initialise too early or too late. It is not immediately obvious that moving a call to GetSingleton() may change the initialisation or destruction order.

Forget about atexit() and friends

You'll have a hard time trying to control the destruction order in a portable manner. Some embedded platforms do not have atexit() support.