Chapter 16. Using Static

FAQ 16.01 What is the purpose of this chapter?

image

This chapter explores the issues related to the situation that occurs when a class contains static data members and/or static member functions.

FAQ 16.02 What are static class members?

Static class members are data and functions that are associated with the class itself, rather than with the objects of the class.

In the following example, class Fred has a static data member x_ and an instance data member y_. There is only one copy of Fred::x_ regardless of how many Fred objects are created (including no Fred objects), but there is one y_ per Fred object. Thus x_ is said to be associated with the class and y_ is said to be associated with an individual object of the class. Similarly class Fred has a static member function f() and an instance member function g().

image

Everything except instance data members must be defined somewhere, such as in the Fred.cpp source file:

image

Static data members are often referred to as class data, and static member functions are often referred to as class services or class methods.

FAQ 16.03 What is an analogy for static data members?

Static data members are like data located in the factory rather than in the objects produced by the factory.

In Detroit, there's a big sign with a running total of the number of cars produced during the current year. But that information isn't under the hood of any given car; all the car knows is a serial number indicating its ordinal number. The total number of cars produced is therefore factory data.

In the following example, class Car is the factory that is used to produce Car objects. Every car has a serial number (serial_). The factory keeps count of the number of cars that have been built via num_, which is a class (or static) datum; serial_ is an object (or instance) datum. The constructors of class Car are responsible for incrementing the number of cars that have been built; for simplicity this number is used as the serial number.

image

Just as a factory exists before it produces its first object, class (static) data can be accessed before the first object is instantiated as well as after the last object has been destroyed.

image

The output is

Car::num_ = 0
Car ctor
Car::num_ = 1
Car ctor
Car::num_ = 2
Car copy
Car::num_ = 3
Car::num_ = 3

Note: See the next FAQ regarding inline functions that access static data members.

FAQ 16.04 Can inline functions safely access static data members?

No!

Static data members should normally be accessed by non-inline functions only (and then only from non-inline functions that are defined in the same source file as the static data member's definition). In some cases inline functions can access static member data, but the programmer needs to think through the issues fairly carefully—they're somewhat tricky.

Suppose class Fred contains two static data members: Fred::i_ is of type int and Fred::s_ is of class string, the standard string class. The data member Fred::i_ is initialized before any code starts running, so Fred::i_ can be accessed from an inline function. However if an inline function accesses Fred::s_, and if the inline function is called from another compilation unit, the inline function might access Fred::s_ before it has been initialized (that is, before the constructor of Fred::s_ has run). This would be a disaster.

Static data members are guaranteed to be initialized before the first call to any non-inline function within the same source file as the static data's definition. In the following example, file Fred.cpp defines both static data member Fred::s_ and member function Fred::f(). This means that Fred::s_ will be initialized before Fred::f() is called. But if someone calls inline function Fred::g() before calling Fred::f(), accessing Fred::s_ could be a disaster since Fred::s_ might not be initialized yet.

Here is file Fred.hpp.

image

Here is file Fred.cpp.

image

Here is an example showing how the above code could possibly fail (this code is assumed to be in a different source file, such as main.cpp).

#include "Fred.hpp"

int main()
{ Fred::g(); }

Note that some—but not all—compilers will initialize static data member Fred::s_ before main() begins. Thus this code is doubly evil since it will subtly fail on some compilers and accidentally work on others. In fact, its success or failure might even depend on the order that object files are passed to the linker, and some visual environments hide the linker so well that many programmers don't even know the order in which object files are passed to the linker.

To make matters worse, the following source file, say even-worse.cpp, calls inline function Fred::g()—therefore accessing Fred::s_—during static initialization. Many compilers will cause this to happen before main() begins, so this is even more likely to cause a problem (but again, the problem will depend randomly on things like the link order, the compiler, the version number, the phase of the moon, etc.).

#include "Fred.hpp"

string x = Fred::g();

FAQ 16.05 What is an analogy for static member functions?

Static member functions are like services attached to the factory rather than services attached to the objects produced by the factory.

image

image

Some services make sense only when applied to an object.

image

Some services make sense only when applied to the factory.

image

Since the factory exists before it produces its first object, the factory can provide services before instantiating an object. That is, fiddleWithClass() can be called before the first Car object is created and/or after the last Car object is destructed:

image

FAQ 16.06 How is a static data member similar to a global variable?

A static data member is like a global variable with a funny name that does not need to be public:.

If a class has a static data member, there is only one copy of that datum even if there are many instances of the class. This is like a global variable. The difference is that a static data member has a funny (scoped) name (it doesn't pollute the global namespace) and it needn't be public: (static data members can be private:, protected:, or public:).

These factors allow classes to be the logical packaging device; source files are reduced to mere buckets of bits and code. There is no need to use source files for hiding data (for instance, there is no need to use file-scope static data to hide data in a source file) since the data can now be hidden in a class. This distinction allows the physical packaging of software to be different from the logical packaging. For example, physical packaging may be optimized based on page fault analysis or on compile-time performance or for maintainability, and so forth.

Global data is rarely used any more. Normally objects and instance data work fine, but when true global data is required, the right choice is normally to use static member data (that is, class-scope static data members) or to put the data in an unnamed namespace.

FAQ 16.07 How is a static member function similar to a friend function?

A static member function is like a friend function with a funny name that needn't be public:.

Static member functions and top-level (C-like) friend functions are similar in that neither has an implicit this parameter, and both have direct access to the class's private: and protected: parts.

Except for overloaded operators, most friend functions end up actually being static member functions, because static member functions have a scoped name (they don't pollute the global namespace) and they don't have to be public:—they can also be private: or protected:.

FAQ 16.08 What is the named constructor idiom?

An idiom that allows a specific name for an operation that is similar to a constructor.

Occasionally, classes have a large suite of overloaded constructors. Because all constructors for a class have the same name, it can be confusing to select between the various overloaded constructors. When this happens, the named constructor idiom may be appropriate.

For example, consider a complex number class, Complex, that supports construction using either polar coordinates (magnitude, angle) or rectangular coordinates (real part, imaginary part). Unfortunately, these constructors are very similar; both constructors take two floats. Should Complex(2,1) be interpreted as specifying polar form (“2 at angle 1”) or as specifying rectangular form (“2 plus 1 times the imaginary constant”)?

Many potential solutions exist to resolve this ambiguity. A boolean flag could indicate which is intended, or an extra dummy parameter on one of the constructors could be used to avoid runtime overhead by making the selection at compile time rather than at runtime. Another solution is to use the named constructor idiom, which is a way of using static member functions to provide alternative constructors for a class. Usually, the named constructor idiom results in user code that is more direct and readable:

image

Both rect() and polar() are static member functions that operate like constructors. Users explicitly call whichever version they want.

image

FAQ 16.09 How should static member functions be called?

Explicitly name the class using ::.

For documentation purposes, calls to static member functions should be coded as Classname::staticMember() rather than as object.staticMember() or ptr->staticMember(). The :: is a reminder that the member function is statically bound (see FAQ 21.09) and that the member function is attached to the class rather than to an individual object of the class.

Calling a static member function Classname::f() from another member function of class Classname is an exception to this rule. In this case, the call can be simply f(), since the meaning is usually clear in this context. For example, when Classname::f() is a protected: static member function of the class, simply write f() rather than Classname::f().

FAQ 16.10 Why might a class with static data members get linker errors?

Static data members must be explicitly defined in exactly one source file.

Here's an example of a header file, such as Fred.hpp.

image

The linker generates an error (“Fred::x_ is not defined”) unless (exactly) one of the source files defines Fred::x_. This definition is normally done in the class's source file, such as file Fred.cpp:

image

Note that the explicit initializer (= 42 in the example) is optional. That is, the line could be changed to

image

Note that even when the static data member is private: or protected:, it must still be explicitly defined as shown in one of the two examples.

FAQ 16.11 How is a const static data member initialized?

A const static data member is declared in the class and is normally defined (and initialized) in a source file, such as a .cpp file. But in some cases it can be initialized in the class body proper. For example, integral types, such as int, unsigned long, char, and so on, are special: they can be initialized where they are declared in the class body proper.

Here is a sample header file, Fred.hpp.

image

Here is corresponding source file, Fred.cpp.

image

Another common style is to use anonymous (unnamed) enums. This style is no longer needed, but it is typical in older C++ code. For example, the static const int i_ = 42 from the previous example can be replaced by enum { i_ = 42 }, as shown in the following example.

image

In either case, the constant is called Fred::i_, and it can be private:, protected:, or public:.

FAQ 16.12 What is the right strategy for implementing a function that needs to maintain state between calls?

Turn the function into a functionoid. Do not create a function with local static data.

In C, it was common to create a function that maintained state between calls by means of local, static data inside the function body. Since this is unsafe in a multithreaded environment, in C++ such a function should be implemented as a functionoid, which is a fancy name for a class that has one major member function. The local static data from the original C-like function should become nonstatic member data of the functionoid class. The benefit is to allow different callers to have different values for the datum that used to be static. For example, every calling function that wants its own copy of the function's state can simply create its own distinct functionoid object.

Viewing a function with local static data as a global functionoid object makes it clear why the static data is expensive to model (global variables aren't fun!). For example, consider the rand() function, which remembers some state between calls:

image

The static variable current introduces subtle dependencies between users of the function. Any change in the calling pattern can alter the behavior of this routine. Such routines are notorious in shared-memory, multithreaded environments.

A better way to do this is with a class. Every user function that wants a pseudorandom stream of numbers can create its own object of this class.

image

The user gets a sequence of random numbers by using the member function next().

image

Before, there was a global rand() function with a single state variable. Now there is a local rand object and as many state variables as there are user functions that want an independent pseudorandom sequence. The dependencies among callers (and especially among the various threads) are eliminated at the source. There is no more shared static data.

Another reason to create a functionoid object is when a function performs several distinct operations. In C, such a function would often accept a what-to-do parameter that selected the operation to be performed. In C++, such a multioperation function should be implemented as an object. Each distinct operation performed by the original function should become a distinct member function on the object. Such an object is also called a functionoid.

FAQ 16.13 How can the function call operator help with functionoids?

The function call operator lets users pretend that the functionoid is a function.

In the previous example, class RandomSequence is a functionoid. Unlike a standard function, RandomSequence can maintain state between calls without sharing that state between all of its callers.

Functionoids often use the function call operator (operator()()) rather than a named member function such as next(). In the following code, next() has been replaced by operator()() in class RandomSequence.

image

Given an object of class RandomSequence called rand, users can now use rand() instead of rand.next():

image

FAQ 16.14 Is it safe to be ignorant of the static initialization order problem?

No, ignorance of the static initialization order problem can result in application crashes.

The static initialization order problem has to do with the lifetimes of class-scope static objects and file-scope or namespace-scope objects. These objects are constructed near the beginning of the application's execution (often before main() begins) and are destructed after main() finishes. The nightmare scenario occurs when there is an order dependency between initializations across different compilation units (that is, different .cpp files). This can be both dangerous and subtle.

For example, suppose a constructor of class Fred uses a static data member of class Wilma, and a user creates a global Fred object. If the static objects in the user's source file are initialized before those in the source file containing Fred's static data member, Fred's constructor will access a Wilma object before it is constructed.

Although this description sounds uncommon, it actually shows up quite often in practice, especially with factory objects whose constructor registers something in a “registry” object. For example, Wilma is actually a map (a registry object) and Fred is a “factory” object whose constructor registers something in the map.

In the following example, the order of the global Fred and the static data member have been arranged to simulate this disaster.

image

The (annotated) output from this program shows that the Wilma object is used before it is initialized. This is a disaster.

image

FAQ 16.15 What is a simple and robust solution to the static initialization order problem?

A very simple and fairly robust solution is to change the static data member into a static member function that returns a reference to a dynamically allocated object. This provides construct on first use semantics, which is desirable in many situations.

The following code shows how to apply this technique to the example from the previous FAQ. The static data member Wilma Fred::wilma_ has been changed to a static member function, Wilma& Fred::wilma(), and all uses of Fred::wilma_ have been changed to Fred::wilma(). Class Wilma is not shown since it is unchanged from the example in the previous FAQ.

image

In the static member function Fred::wilma(), pointer p is static, so the new Wilma() object is allocated only the first time that Fred::wilma() is called. All subsequent calls simply return a reference to the same Wilma object.

As shown in the (annotated) output from this program, the Wilma object is initialized before it is used. This is good.

image

FAQ 16.16 What if the static object's destructor has important side effects that must eventually occur?

image

One limitation of the technique described in the previous FAQ is that it abandons the static Wilma object on the heap—the Wilma object is never destructed. If the Wilma object's destructor has important side effects that should eventually happen, then the implementation of Fred::wilma() needs to be changed so that it simply returns a local static object by reference.

The following code shows how to apply this technique to the example from the previous FAQ. The local static pointer static Wilma* p = new Wilma(); has been changed to simply static Wilma w;, and the return statement simply returns the local static object w. Class Wilma is not shown since it is unchanged from the example in the previous FAQ.

image

Since the local static object w is static, it is initialized only the first time control flows over its declaration, that is, the first time Fred::wilma() is called. This is the same construct on first use semantics as was described in the previous FAQ, which is normally quite desirable.

Unfortunately, this solution has its own problems. Remember why this solution was proposed in the first place: the Wilma object's destructor has important side effects that need to eventually occur. Although this second solution guarantees that they will occur (assuming the Fred::wilma() function is called at least once), it introduces a new problem that the previous solution did not have: a static deinitialization order problem. In particular, if some static object's destructor calls Fred::wilma(), Murphy's Law says that that call will occur after the static Wilma object has been destructed. If that may occur, the best solution is the nifty counter technique, which is described in the next FAQ.

FAQ 16.17 What if the static object's destructor has important side effects that must eventually occur and the static object must be accessed by another static object's destructor?

image

This is the most restrictive of all scenarios since it means that the construction must occur before the object is first used, and it must be destructed after its last use. The solution is called the nifty counter technique. What happens is that a static counter is created (the nifty counter) along with a static object in each source file whose constructor increments this nifty counter and whose destructor decrements the nifty counter. When the nifty counter is incremented from zero, the static object is initialized, and when the nifty counter is decremented to zero, the static object is destructed.

The following code shows how to apply this technique to the example from the previous FAQ. The static data member is back, but this time it is a static pointer. Nested class Fred::Init has a static counter (the nifty counter) called count_, which is incremented by Fred::Init's constructor and decremented by Fred::Init's destructor. The Fred::wilma_ object is created when the nifty counter is incremented from zero and is destructed when the counter is decremented back to zero. Class Wilma is not shown since it is unchanged from the example in the previous FAQ.

Here is the header file Fred.hpp.

image

Here is the source file "Fred.cpp":

image

Every source file that includes header file Fred.hpp ends up with its own static Fred::Init object called fredInit. Since this static object appears very early in the source file, it is initialized before most other static objects in the source file (in particular, it is guaranteed to be initialized before any static object in the source file could call Fred::Fred(), since the call to any member function of class Fred can occur only after the header of class Fred has been #included).

Of all the source files that include header file Fred.hpp, one of them, say foo.cpp, is initialized first. During the static initialization of foo.cpp, the nifty counter Fred::Init::count_ is incremented from zero, and the Fred::wilma_ object is created. Since the Fred::wilma_ object is initialized before any calls to any member functions of class Fred can be made, it is guaranteed to be constructed before it is used.

The static deinitialization situation is similar but opposite. Of all the source files that include Fred.hpp, one of them, say foo.cpp, is the last one to be deinitialized. Since deinitialization occurs in bottom to top order, the static Fred::Init object in file foo.cpp is one of the last things that is destructed (certainly it is destructed after any static object could call any member function of class Fred). Therefore the Fred::wilma_ object is destructed just after the last static object could possibly use it: it will not be used after it has been destructed.

Unfortunately the nifty counter technique also has problems. Although it never allows an object to be used either before construction or after destruction, it does force a small amount of static initialization code into every source file that includes header file Fred.hpp. This means that a large percentage of the application needs to be paged into memory during startup, which can significantly degrade startup performance, especially if there are a lot of source files that include headers that use the nifty counter technique.

FAQ 16.18 What are some criteria for choosing between all these various techniques?

image

Here are the pros and cons of each of the three techniques that were presented.

Construct on first use with new: Users access the static object via a non-inline access function. The access function has a local static pointer and allocates the object via new. Pro: The technique is easy to remember, simple to use, efficient during startup, safe during startup, and safe during shutdown. Con: The static object is abandoned on the heap—if the static object's destructor has important side effects that must occur, this technique cannot be used. (See FAQ 16.15.)

Construct on first use with a local static object: Users access the static object via a non-inline access function. The access function has a local static object. Pro: The technique is easy to remember, simple to use, efficient during startup, and safe during startup, and it eventually destructs the object. Con: The technique is not safe during shutdown—if a static object's destructor calls the access function, the static object could be accessed after it is destroyed, which would be a disaster. (See FAQ 16.16.)

Nifty counter: The static object is constructed when the nifty counter is incremented from 0 and is destructed when the nifty counter is decremented to 0. Pro: The technique is simple to use, safe during startup, and safe during shutdown, and it eventually destructs the object. Con: Potential performance problem during startup and shutdown. (See FAQ 16.17.)

In our experience, a significant percentage of static objects that should be constructed before they are first used are registry objects (for example, a map object that will be populated during static initialization by other static objects). In most of these cases, the first technique is sufficient since the map object rarely has to be destructed—it can be abandoned on the heap. This is good news because the first technique is easy to use, easy to remember, is fast, and is safe during both startup and shutdown.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset