Discussion: All Good Things Must Come to an End

Execute the code to understand the output and gain insights into constructors with local state variables.

Run the code

Now, it’s time to execute the code and observe the output.

Press + to interact
#include <iostream>
#include <string>
struct Connection
{
Connection(const std::string &name) : name_(name)
{
std::cout << "Created " << name_ << "\n";
}
~Connection()
{
std::cout << "Destroyed " << name_ << "\n";
}
std::string name_;
};
Connection global{"global"};
Connection &get()
{
static Connection localStatic{"local static"};
return localStatic;
}
int main()
{
Connection local{"local"};
Connection &tmp1 = get();
Connection &tmp2 = get();
}

A quick recap

In the Hack the Planet! and Going Global puzzles, we learned about the lifetime and initialization differences between a global variable and a local variable:

Press + to interact
int id; // global variable, one instance throughout the whole program
void f()
{
int id = 2; // local variable, created anew each time `f` is called
}

In this puzzle, we add two new interesting aspects to the object lifetime:

  • We no longer initialize the variables to a simple constant; there is a constructor with a side effect.

  • We introduce a local variable localStatic with static storage duration.

So, how does the behavior change when we introduce a constructor and a local static variable?

Global variable

Let’s first consider the global variable global. Like the global variable id we saw previously, it has static storage duration, which means only one instance is alive from its initialization until its destruction at the very end of the program.

We previously saw that the global int id; without an initializer was zero-initialized. We also mentioned that a global with an initializer int id = 2; would be constant-initialized. Both are examples of static initialization, which happens as part of program initiation. The global variable global, on the other hand, is initialized with a non-constant expression. The Connection constructor is not constexpr (it can’t be since it prints to std::cout). Objects that can’t be statically initialized are instead dynamically initialized, which happens later.

Dynamic initialization

When does dynamic initialization happen, though? It’s commonly believed that dynamic initialization happens before main()—and for good reason: this is a simple and common way to implement dynamic initialization. For instance, Linux has a __libc_start_main() function, which gets called before main(). The __libc_start_main() function invokes global constructors before eventually calling the main() function.

The C++ standard doesn’t require dynamic initialization to happen before main(), it merely allows it. However, the standard does require dynamic initialization of global variables to happen before any functions defined in the same translation unit are called. A translation unit (TU) is just a single .cpp file after all the #include files have been included. So in this puzzle, where main() is defined in the same TU as global, global is actually guaranteed to be initialized before main()!

The complete rules for initialization order are rather long and complicated. https://en.cppreference.com/w/cpp/language/initializationCppReference has an introduction, and the C++ standard has all the details in [basic.start]https://timsong-cpp.github.io/cppwp/std20/basic.start.

Static local variable

Next, let’s look at localStatic. Like global, the localStatic also has static storage duration, so there’s only one instance of localStatic, which is alive from its initialization until the end of the program. Unlike globals, local static variables are not initialized when the program starts, but rather, the first-time control passes through their declarations. So even though localStatic has static storage duration, it’s not initialized until the first time get() is called.

Local variable

Finally, local is a simple local variable with automatic storage duration; it is initialized each time control passes through its declaration.

Understanding the output

Let’s look at the execution of this program then:

  • The program starts, and right before main() is called, global is initialized.

  • We enter main() and local is initialized.

  • We call get() for the first time and localStatic is initialized.

  • We call get() a second time, but since localStatic is already initialized, it’s not initialized again.

But we’re not done yet! We also have to figure out when our three objects are destroyed. Luckily, the rules for destruction are a lot simpler. Local objects with automatic storage duration are destroyed at the end of the scope in which they were declared. Objects with static storage duration are destroyed at the end of the program in the reverse order of their creation:

  • When we exit main(), local goes out of scope and is destroyed.

  • The localStatic was the last object with static storage duration to be initialized, so it’s destroyed first.

  • The global was initialized first and is destroyed last.

Using a static local instead of a global is a good way to get better control over the lifetime of a global variable. Instead of referring to a global object each time we need it, we call a function to get a reference to it, and now we have control over when the object is initialized! This can be particularly useful if we have multiple globals depending on each other.

Finally, a word of caution regarding globals defined in multiple TUs: although there’s some order to the initialization of globals inside a single TU, globals in different TUs can be initialized in any order. For instance, we might expect the following program to print 1, but it prints 0.

Press + to interact
main.cpp
source2.cpp
Value.h
#include "Value.h"
#include <iostream>
extern Value value1;
Value value2{value1}; // No guarantee that `value1` has been initialized yet!
int main()
{
std::cout << value2.i;
}

This problem of unknown initialization order between globals in different translation units is often referred to as the Static Initialization Order Fiasco (SIOF).

Recommendations

Here are some recommendations for handling global variables safely:

  • Avoid globals depending on each other: Be cautious with globals that depend on each other, especially if they’re defined in different files. This can lead to initialization order issues.

  • Use Clang-Tidy: Utilize this with the cppcoreguidelines-interfaces-global-init check, which can detect some problematic uses of dependent globals.

  • Prefer local static variables: Instead of using global variables, use functions with local static variables to control lifetimes more predictably and safely.

Level up your interview prep. Join Educative to access 70+ hands-on prep courses.