Magic Statics
Magic statics, more formally known as function-local static initialization, is the term used for the fact that the initializer of any function-local static variable is guaranteed to be thread-safe, even if the function is entered concurrently by multiple threads at once. Function-local static means a variable which is declared inside a function with the static
modifier. For example, below is a function-local static variable:
void someFunction(){ static int magicStatic = 0;}
If another thread enters the same function while the first thread is still initializing the static variable, the compiler will make sure the second thread will block until the first thread has finished initializing (and also make sure the second thread does not initialize the static variable again). This essentially means the compiler has to use a mutex (or similar mechanism) to lock the initialization of the static variable. This is where the “magic” comes from, as the compiler inserts a synchronization mechanism “behind the scenes” to make this work.
§6.7 [stmt.dcl] p4: If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.
The example below shows the basic thread-safe behavior of function-local statics. counter()
is called from both thread t1
and t2
, each passing in a different value to initialize the static variable i
with. The compiler ensures only the first thread will initialize the variable. Both threads will print either the value of 1
or 2
as being returned from counter()
(when I tested it, I always got the value 1
…presumably because thread t1
is created first).
#include <thread>#include <iostream>
int counter(int init) { static int i = init; return i;}
int main() { auto t1 = std::thread([](){ int i = counter(1); std::cout << "t1: " << i << std::endl; }); auto t2 = std::thread([](){ int i = counter(2); std::cout << "t2: " << i << std::endl; }); t1.join(); t2.join();}
Before C++11, calling counter()
from multiple threads is classified as undefined behaviour (UB). When I tested this code, the compiler made use of __cxa_guard_acquire
and __cxa_guard_release
to make sure the initialization is thread-safe.
Existing After The Function Returns
Another neat property of function-local static variables is that they continue to exist after the function returns (technically this have nothing to do with the “magic” aspect, but it’s useful to discuss at the same time). This is not the case for normal (non-static) local variables, which are destroyed when the function returns. This means you can return a reference to a function-local static variable to the caller. For example:
std::string& getString() { // Create a local static variable static std::string magicStatic("Hello"); // Return a reference to it! :-O return magicStatic;}
The above code creates a new static qualified string inside the function, and then returns a reference to it. Seeing a reference being returned to a locally defined variable should give you the heebie jeebies! Always make sure that you understand it is only allowed because of the static
qualifier.
Singletons
Magic statics allows for alternative implementation of the Singleton pattern.
class Singleton final{public: static Singleton& GetInstance();
private: Singleton() = default; ~Singleton() = default;
// Delete the copy and move constructors Singleton(const CSingleton&) = delete; Singleton& operator=(const CSingleton&) = delete; Singleton(CSingleton&&) = delete; Singleton& operator=(CSingleton&&) = delete;};
Singleton& Singleton::GetInstance(){ // Let's create a magic static static Singleton instance; return instance;}
The constructor and destructor is made private, and both the copy and move constructors deleted, so that this class cannot be duplicated/transferred in anyway.
This is sometimes called the Meyers’ Singleton after Scott Meyers publication “C++ and the Perils of Double-Checked Locking”.
Note that this only guarantees that the initialization is thread-safe. As mentioned before, all subsequent access to the variable from multiple threads must be synchronized manually by the programmer.
Lazy Initialization
Magic statics allows for the concept of lazy initialization. This means that the object only gets created if it is used (e.g. in the example above, by calling GetInstance()
).
Hiding Global Statics
Magic statics can be used to hide otherwise globally defined static variables. In general, it is a bad idea to define global objects, and making the user call a function to retrieve the static instead resolves this issue.
Vendor Support
Most C++ compilers support magic statics, but you have to be careful about the thread safety aspect, as this was only introduced into the C++ standard in C++11! Any C++11 compliant version of GCC will support thread-safe magic statics.
On the Windows side of things, thread safety was implemented for magic statics in Visual Studio 2015.

Recursive Calls
Make sure that the function that initializing the static variable does not call itself while the static variable is being initialized (i.e. recursively), as this behaviour is undefined.
§6.7 [stmt.dcl] p4: If control re-enters the declaration recursively while the variable is being initialized, the behavior is undefined.
It is ok to recursively call the function as long as it’s not recalled during the initialization of the static variable.
Compiler Options
GCC provides the -fno-threadsafe-statics
option to prevent the compiler from generating the extra code surrounding function-local static variables to make sure the initialization is thread-safe.1
In MSVC you can disable magic statics with the /Zc:threadSafeInit-
compiler option (note the dash suffix, to disable it). This option is on by default. Thread-safe initialization of static local variables relies on code implemented in the Universal C run-time library (UCRT). Disabling this will remove the dependency on the UCRT.2
Non Function-Local Statics
Non-local initialization (e.g. a static
variable declared outside of a function) is usually safer from concurrency issues due to the initialization occurring before main()
is called and thus usually before any threads are created by the user. However, it is not immune! Non-local initialization also has to be concerned about concurrency issues. There are two sources of non-local concurrency:3
- Dynamic opening of a shared library in a multi-threaded context.
- The creation of multiple threads in initializers, and thus the potential concurrent execution of other initializers.
The document “Dynamic Initialization and Destruction with Concurrency” by Lawrence Crowl discusses local and non-local initialization in good detail. It also discusses destruction, which has similar issues to initialization.3
Footnotes
-
GNU GCC. Options Controlling C++ Dialect [documentation]. Retrieved 2025-04-18, from https://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Dialect-Options.html. ↩
-
C++ in Visual Studio Reference (2021, Mar 8). Learn > C++, C, and Assembler > /Zc:threadSafeInit (Thread-safe Local Static Initialization) [documentation]. Microsoft. Retrieved 2025-04-19, from https://learn.microsoft.com/en-us/cpp/build/reference/zc-threadsafeinit-thread-safe-local-static-initialization?view=msvc-170. ↩
-
Lawrence Crowl. Dynamic Initialization and Destruction with Concurrency [document]. Open Standards. Retrieved 2025-04-22, from https://open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2660.htm. ↩ ↩2