Previous section   Next section

Imperfect C++ Practical Solutions for Real-Life Programming
By Matthew Wilson
Table of Contents
Chapter 11.  Statics


11.1. Nonlocal Static Objects: Globals

Despite the fact that the language clearly defines the relationships between the initialization phases and the mainline execution, the use of nonlocal static objects has several pitfalls (see section 15.5) and is generally not recommended [Sutt2000, Dewh2003]. The main problem with their use relates to ordering, and we'll look at this now.

The ordering problem is comprised of two closely related issues. The first problem is that it is possible to have cyclic interdependencies between two or more static variables. This is a fundamental engineering issue, and there's no solution to it, but there are ways in which it can be made more detectable.

The second problem is that it is possible to reference a nonlocal static variable before it is initialized, or after it has been uninitialized. This is a constant source of consternation for developers, and a constant topic of conversation on newsgroups, and may be said to be an imperfection of C++.

Imperfection: C++ does not provide a mechanism for controlling global object ordering.


11.1.1 Intracompilation Unit Ordering

Within a given compilation unit, the lifetime of global objects obeys the same ordering as do stack objects: they are constructed in the order of their definition, and destroyed in reverse order (C++-98: 3.6.2;1). Note that it is the order of definition, not declaration, that is important. In Listing 11.2, the order of construction is o1, o2, o3, o4, and the order of destruction is o4, o3, o2, o1.

Listing 11.2.


class Object;





extern Object o2;





Object  o1("o1");


Object  o2("o2");





int main()


{


  Object  o3("o3");


  Object  o4("o4");





  return 0;


}



When the static objects are in the process's compilation unit, they are constructed prior to the entry to main() and destroyed after main() returns.

It is entirely legal and proper to have one global object dependent on another that has already been defined within the same link unit. Hence o2 could have been defined as a copy of o1.



extern Object o2;





Object  o1("o1");


Object  o2(o1);   // This is fine



However, though the compiler will let you do so, it is not valid to have a dependency on another that has not yet been defined, even if it has been declared. The following leads to undefined behavior:



extern Object o2;





Object  o1(o2);   // Undefined!


Object  o2("o2");



Since the space for global objects is preallocated and zero initialized, o1 is passed the correct address of o2, but the members of o2 are all zero. Depending on the definition of Object, this may cause a crash or merely result in a silent error. In some circumstances it will produce correct behavior, but it is still a serious mistake to rely on it.

11.1.2 Intercompilation Unit Ordering

When it comes to the issue of ordering of globals between compilation units, we're firmly in implementation-defined territory. In practice this comes down to the linker. For most linkers, the global objects are ordered according to the order of linking of the compilation units. Consider Listing 11.3.

Listing 11.3.


// object.h


class Object { . . .};


extern Object o1;


extern Object o2;


extern Object o3;





// main.cpp


#include "object.h"


Object o0("o0");


Object o1("o1");


int main() { . . . }





// object2.cpp


#include "object.h"


Object o2("o2");





// object3.cpp


#include "object.h"


Object o3("o3");



Stipulating the object files for object1.cpp, object2.cpp, and object3.cpp in that order to the linker, the ordering for several compilers is shown in Table 11.1:

Table 11.1.

Compiler/Linker

Order

Borland C/C++ 5.6

o0, o1, o2, o3

CodeWarrior 8

o0, o1, o2, o3

Digital Mars 8.38

o3, o2, o0, o1

GCC 3.2

o3, o2, o0, o1

Intel C/C++ 7.0

o0, o1, o2, o3

Visual C++ 6.0

o0, o1, o2, o3

Watcom C/C++ 12

o3, o2, o0, o1


Evidently these compilers operate two clear, but opposing, strategies. Borland, CodeWarrior, Intel, and Visual C++ cause global objects to be constructed in the order corresponding to the object file link order. Digital Mars, GCC, and Watcom do the reverse.

This inconsistency causes a problem. If all compilers/linkers supported a standard global object ordering mechanism, it would be possible to rely on a predictable global object ordering in your application.

Doing so is a fragile thing, to be sure, since the correctness of your code depends on something external to it: the ordering of object files in the makefile/project-file. It would take very little "effort" in a large project to break such dependencies, and such breakage could be very difficult to diagnose, or even detect.

Nonetheless, relying on linker-controlled object file ordering is, in principle, a way of achieving static object ordering. If you are sufficiently confident of your development team, and the stability of your tools, you may choose to employ this technique. The difficulty remains, however, of validating that your build project(s) continue to reflect the required linker ordering through the lifetime of your product.

One practical measure of this is to insert debugging code into each compilation unit, in debug builds at least, to trace out the initialization order. Since we know that the order within a given compilation unit is fixed, and that the objects are either all initialized prior to main(), or prior to the first use of any one of them, all we need to do is to inject a tracing, nonlocal, static object at the beginning or end of each compilation unit, and we will be able to unambiguously determine the linker ordering.

Let's look at how this can be done. CUTrace.h contains the declaration of the function CUTraceHelper(), which prints the file being initialized, a message, and some optional arguments. It might be something like ". . . cu_ordering_test.cpp: Initialising." The other function is CUTrace(), which simply takes the message and arguments, and passes them, along with the file, to CUTraceHelper():



// CUTrace.h


extern void CUTraceHelper(char const *file, char const *msg, va_list args);





namespace


{


  void CUTrace(char const *message, ...)


  {


    va_list args;


    va_start(args, message);


    CUTraceHelper(__BASE_FILE__, message, args);


    va_end(args);


  }


  . . .


} // namespace



There are two important features of this. The first is that CUTrace() is defined in an anonymous namespace, which means that each compilation unit gets a copy of the function (see section 6.4). However, this is not a concern, since compilers can easily optimize it down to the call to CUTraceHelper(). In any case, such a thing would likely only be used in debug and test versions, rather than the full release. Without the static, the linker would complain about multiple definitions, whereas using inline would just result in all but one version being elided by the linker.

The second feature is the use of the nonstandard symbol __BASE_FILE__. Digital Mars and GCC both define this symbol as the name of the primary implementation file for which the compilation is taking place, generally the file named on the command-line. Thus, even though CUTrace() is defined in the header CUTrace.h, it will pass the name of the primary implementation file through to CUTraceHelper().

Naturally, since this symbol is nonstandard, this technique does not work in its current form for other compilers. The answer is to provide __BASE_FILE__ for other compilers. Admittedly it's verbose, but it works, and it's easy to insert it with a Perl, Python, or Ruby script.[2] And let's be honest: if you're going to the extreme measure of relying on linker ordering, this extra bit of code in each source file won't be the top of your list of concerns.

[2] I've included a reasonably competent one on the CD.



// SomeImplFile.cpp


#ifndef __BASE_FILE__


static const char __BASE_FILE__[] = __FILE__;


#endif /* __BASE_FILE__ */


#include "CUTrace.h"


. . .



The last part of the picture uses internal linkage again, this time to ensure that we get a single copy of the class CUTracer.



// CUTrace.h


. . .


namespace


{


  . . .


  static CUTracer   s_tracer;


} // namespace



Note that this also works if you declare CUTrace() and s_tracer static (see section 6.4), but the anonymous namespace is the better option as long as you don't need to support any old compiler relics.

Even if you wisely don't want to get into linker ordering as a means of developing your program—and let's be frank: who wants to write code whose correctness depends on observed behavior?—this technique can still be a very useful diagnostic aid, so I'd recommend your including it on big projects, even if I wouldn't recommend that you rely on linker ordering in your work.

Recommendation: Don't rely on global object initialization ordering. Do utilize global object initialization ordering tracing mechanisms anyway.


11.1.3 Avoid Globals, in the main()

Obviously, one can avoid all these problems by simply not having any global variables, but there are just some times when you cannot avoid the need for them. A simple, albeit inelegant, way to avoid them is to change your global objects into stack objects within main(), from where you can explicitly control their lifetimes, and give out pointers to them to the other code in your executable. Listing 11.4 shows how this can be achieved.

Listing 11.4.


// global1.h


class Global1


{


  . . .


};


extern Global1 *g_pGlobal1;





// global2.h


class Global2


{


  . . .


};


extern Global2 *g_pGlobal2;





// main.cpp


#include "global1.h"


#include "global2.h"





int main(. . .)


{


  Global1  global1;


  g_pGlobal1 = &global1;





  Global2  global2;


  g_pGlobal2 = &global2;





  return g_pGlobal2->Run(. . .)


}



The obvious drawback to this is that you have to use all the global variables by pointer, which can lead to a small loss of efficiency to go along with the small syntactic inconvenience. A more insidious drawback is that if any of the client code of the objects somehow itself makes use of global variables and uses the main()-based, faux-global variables outside of main(), your program will crash in a heap. But where it is manageable you get full control over your static objects' lifetimes and ordering, so it's sometimes worth the effort.

11.1.4 Global Objects Coda: Ordering

In the general case, managing global object ordering in a predictable and portable fashion is very difficult, if not impossible. However, we'll see in the next section that things are a little bit more tractable when dealing with singletons (see section 11.2). It is possible to adapt one of the solutions to the singleton ordering problem—the counted-API solution—to the issue of global objects if each global object of a given type can be associated with a unique compile-time identifier, but that's getting outside the scope of this chapter. Indeed, to solve that problem requires so many leaps through burning hoops that it's better to step back and consider whether the right questions were asked at the design stage.


      Previous section   Next section