When working with static or global variables in C or C++, we know the compiler allocates storage for them in the application's data segment. However, when the variable is an instance of a C++ class, it must be initialized as well as allocated. While the C++ language specification explains when this initialization takes place, it is a rather simple matter to demonstrate the rules for clarity.

The following code listing comes from a Windows console program written with Microsoft Visual Studio. It contains several Tracker objects which will help us trace the initialization of static and automatic objects.

#include "stdafx.h"

#include <iostream>
#include <string>

// The tracker class will simply stream output to std::cout so
// we can watch the execution flow of our test program.
class Tracker
{
public:
    Tracker(const std::string& inName)
        : mName(inName)
    {
        std::cout << "create " << mName << "n";
    }

    ~Tracker()
    {
        std::cout << "destroy " << mName <<"n";
    }

private:
    const std::string mName;
};

// We'll place static variables inside and around a namespace scope
// to see if scoping affects initialization order.

static Tracker t1("before namespace");

namespace test
{
    static Tracker t2("within namespace");

    void test()
    {
        // We'll also define a static variable within a function
        // to see when it gets initialized.
        static Tracker t3("within function");
    }
}

static Tracker t4("after namespace");

// Our main function will contain static and dynamic Tracker
// instances to see when they are initialized.

int _tmain(int argc, _TCHAR* argv[])
{
    std::cout << "enter mainn";

    Tracker m1("dynamic in main before test");

    static Tracker m2("static in main before test");

    test::test();

    Tracker m3("dynamic in main after test");

    static Tracker m4("static in main after test");

    std::cout << "exit mainn";

    return 0;
}

Here's the output from our program:

create before namespace
create within namespace
create after namespace
enter main
create dynamic in main before test
create static in main before test
create within function
create dynamic in main after test
create static in main after test
exit main
destroy dynamic in main after test
destroy dynamic in main before test
destroy static in main after test
destroy within function
destroy static in main before test
destroy after namespace
destroy within namespace
destroy before namespace

So, what we can observe about static initialization?

  1. Local static objects are initialized when they are first used (m2 and m4).
  2. All local objects are initialized in the order they are used (m1, m2, m3, then m4).
  3. Namespaces don't affect initialization order.
  4. Non-local static objects are initialized when your program starts (t1, t2, and t4).
  5. Non-local static objects are initialized in the order they are declared within a file (t1, t2, then t4).

Again, this is all explained in the C++ Standards documents, but sometimes running simple test code is easier than parsing the standards. There's one thing we did not try to observe here: when are static objects in one source file initialized relative to static objects in another source file? If you run some tests, you may see consistent results, but don't depend on them. The exact behavior will vary between compilers as object files are linked together. There's no simple way to guarantee the order in which these objects across multiple files will be initialized. We only know that objects within a file will be initialized in the order in which they are declared.

Back to the initialization of locally scoped static objects...exactly how does the compiler handle this lazy form of initialization? Let's look at the disassembled code from test::test(). I've trimmed out some of the listing for brevity and added comments to explain how things work:

    void test()
    {
        // I've removed the entry code.

        static Tracker t3("within function");

        // Check the initialization flag on the t3 object. If it's
        // already initialized, we can skip it (jne to 0B1597h will
        // leave the method since there's nothing else in here).
000B151D  mov         eax,dword ptr [$S1 (0BC1ACh)]
000B1522  and         eax,1
000B1525  jne         test::test+0B7h (0B1597h)

        // Set the initialization flag so we don't re-initialize the
        // next time we come through here.
000B1527  mov         eax,dword ptr [$S1 (0BC1ACh)]
000B152C  or          eax,1
000B152F  mov         dword ptr [$S1 (0BC1ACh)],eax

        // Perform initialization by calling the class constructor.
000B1534  mov         dword ptr [ebp-4],0
000B153B  mov         esi,esp
000B153D  push        offset string "within function" (0B9910h)
000B1542  lea         ecx,[ebp-0F0h]
000B1548  call        dword ptr [__imp_std::basic_string<char,std::char_traits<char>,std::allocator<char> >::basic_string<char,std::char_traits<char>,std::allocator<char> > (0BD304h)]
000B154E  cmp         esi,esp
000B1550  call        @ILT+425(__RTC_CheckEsp) (0B11AEh)
000B1555  mov         byte ptr [ebp-4],1
000B1559  lea         eax,[ebp-0F0h]
000B155F  push        eax
000B1560  mov         ecx,offset t3 (0BC18Ch)
000B1565  call        Tracker::Tracker (0B1073h)

        // I've removed some clean-up code.
    }
000B1597  mov         ecx,dword ptr [ebp-0Ch]
000B159A  mov         dword ptr fs:[0],ecx
000B15A1  pop         ecx
000B15A2  pop         edi
000B15A3  pop         esi
000B15A4  pop         ebx
000B15A5  add         esp,0F4h
000B15AB  cmp         ebp,esp
000B15AD  call        @ILT+425(__RTC_CheckEsp) (0B11AEh)
000B15B2  mov         esp,ebp
000B15B4  pop         ebp
000B15B5  ret

One potentially important thing worth noting about the lazy initialization of locally scoped static variables: the initialization is not thread-safe! You can see there's a small window between reading the initialization flag (at address 000B151D) and updating the initialization flag (at address 000B152F). If you don't protect the initialization with a locking mechanism, the object may be initialized more than once (and debugging any problems may prove difficult if you're not aware of how static initialization occurs). One way to avoid this problem would be to move the declaration so it is static within the file or namespace, ensuring it is initialized only on the application's main thread at startup. However, this does make the object accessible from outside the method that uses it, which may or may not be desirable.