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.
#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:
int id; // global variable, one instance throughout the whole programvoid 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.
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()
andlocal
is initialized.We call
get()
for the first time andlocalStatic
is initialized.We call
get()
a second time, but sincelocalStatic
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
.
#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.