When does static
Object Initialization Occur?
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?
- Local static objects are initialized when they are first used (
m2
andm4
). - All local objects are initialized in the order they are used (
m1
,m2
,m3
, thenm4
). - Namespaces don't affect initialization order.
- Non-local static objects are initialized when your program starts (
t1
,t2
, andt4
). - Non-local static objects are initialized in the order they are declared within a file (
t1
,t2
, thent4
).
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.