Share via

Static initialization of thread_local variable with heap-allocating constructor crashes program

Marco Fischer 0 Reputation points
2026-05-29T09:08:43.94+00:00

I noticed that the following static variable in my C++ code caused my program to stop working (crash / exception at startup):

  • static thread_local std::unordered_map<std::string, int> myMap;

Debugging the issue, an exception was raised while the unordered_map's constructor is allocating memory on the heap.

And indeed, it looks like the combination of "thread_local" + static initialization + heap allocation is causing trouble. This is the minimal program to reproduce the issue:

#include <new>
#include <stdexcept>
#include <iostream>

struct ForceEH {
  int* a;
  ForceEH() {
    try {
      a = new int;
    } catch (...) {
      throw;
    }
  }
};

static thread_local ForceEH ohNo;

int main() {
  std::cout << "Hello World." << std::endl;
  return 0;
}

I tried to build this using VS 2017 and VS 2022 and in both cases I get an executable that works only on some Windows 11 versions. Windows 11 22H2 seems to be okay, while it crashes in Windows 11 23H2. On some Windows 11 24H2 it is working on some it's crashing. Windows 11 25H2 seems to be fine.

Developer technologies | C++
Developer technologies | C++

A high-level, general-purpose programming language, created as an extension of the C programming language, that has object-oriented, generic, and functional features in addition to facilities for low-level memory manipulation.


2 answers

Sort by: Most helpful
  1. Gade Harika (INFOSYS LIMITED) 2,850 Reputation points Microsoft External Staff
    2026-05-29T12:03:54.0266667+00:00

    Thanks for reaching out.
    This behavior is not tied to an officially documented list of supported Windows 11 versions for thread_local initialization. At present, Windows release health for Windows 11 23H2 shows no active known issues related to this scenario. [learn.microsoft.com]

    From Microsoft documentation and related issues, the problem is more likely caused by runtime/toolset mismatch or TLS initialization behavior, not the OS version itself.

    Root cause considerations

    • On Windows/MSVC, thread_local is implemented using TLS (__declspec(thread)), and objects with constructors require runtime initialization support, which has known caveats. [learn.microsoft.com], [learn.microsoft.com]
    • Microsoft explicitly states that:
      • Applications built with a newer toolset must be deployed with a Visual C++ Redistributable that is at least as new as the newest toolset used.

    Update the Visual C++ Redistributable

    • Install the latest supported version from Microsoft:
      • Download latest VC++ Redistributable
        • Microsoft recommends using the latest version for all apps built with VS 2017–2026 toolsets. [learn.microsoft.com]
        Check deployment consistency
        - Ensure no **older `msvcp140.dll` or CRT binaries** are shipped alongside your app.
        
           - Ensure all EXE/DLL components are built with a **consistent toolset version**.
        
    1. Avoid global thread_local dynamic initialization (workaround)
      • Replace global/static TLS objects with function-local initialization:
        ForceEH& GetForceEH() {     thread_local ForceEH instance;     return instance; }
        or
        std::unordered_map<std::string, int>& GetMyMap() {     thread_local std::unordered_map<std::string, int> myMap;     return myMap; }
        This defers initialization until first use and avoids startup-time TLS initialization issues.

        Summary

      • There is no official Windows version compatibility list for this issue.
      • The most common cause is VC++ runtime mismatch across machines.
      • Updating the Redistributable and ensuring consistent builds typically resolves the crash.
      • As a best practice, prefer function-local thread_local initialization over global/static initialization.
      Please let us know if you require any further assistance, we’re happy to help. If you found this information useful, kindly mark this as "Accept Answer". So that others in the community facing similar issues can easily find the solution. Your contribution is highly appreciated.

    Was this answer helpful?


  2. AI answer

    2026-05-29T09:09:25.03+00:00

    The behavior comes from how MSVC implements thread_local and thread-safe initialization of local/static objects, and from OS-level limits and bugs around thread-local storage (TLS) and heap usage. The key points from the provided documentation:

    1. thread_local objects and TLS implementation
    • On Windows, thread_local is functionally equivalent to __declspec(thread) for native C++ code.
    • thread_local can only be applied to objects with static storage duration (global, static local, static data members). Any block-scope thread_local is implicitly static.
    • Each thread gets its own instance, created when the thread starts and destroyed when it exits.

    Reference behavior:

    "A variable declared with the thread_local specifier is accessible only on the thread on which it's created. The variable is created when the thread is created, and it's destroyed when the thread is destroyed. Each thread has its own copy of the variable. On Windows, thread_local is functionally equivalent to the Microsoft-specific __declspec( thread ) attribute."

    This means the runtime and OS must set up TLS slots and run constructors early in thread startup, which can interact with heap allocation and OS TLS limits.

    1. Thread-safe initialization and TLS usage

    MSVC’s implementation of thread-safe initialization for static locals uses TLS internally:

    "Thread-safe static local variables use thread-local storage (TLS) internally to provide efficient execution when the static has already been initialized. The implementation of this feature relies on Windows operating system support functions in Windows Vista and later operating systems."

    Even though the example uses a global static thread_local object, the same TLS machinery is involved. The initialization code runs very early and uses TLS and the heap. If there is an OS bug or a TLS limit/interaction in a specific Windows build, heap allocation during TLS initialization can trigger an access violation or similar crash.

    1. OS-level TLS limits and crashes

    The documentation explicitly notes that exceeding TLS-related limits or issues in older OSes can cause crashes:

    "These operating systems also have a lower limit on the number of TLS sections that can be loaded. Exceeding the TLS section limit can cause a crash. If this is a problem in your code … use /Zc:threadSafeInit- to disable the thread-safe initialization code."

    While that remark is about older Windows versions, it shows that TLS initialization and heap use are sensitive to OS behavior and limits. A regression or change in specific Windows 11 builds can therefore manifest as crashes when a thread_local object’s constructor allocates from the heap.

    1. Managed/WinRT restriction (for completeness)

    In managed (/clr) or WinRT (/ZW) code, dynamic initialization of thread_local or __declspec(thread) is not allowed:

    "In managed or WinRT code, variables declared by using the __declspec(thread) storage class modifier attribute or the thread_local storage class specifier cannot be initialized with an expression that requires evaluation at run-time. A static expression is required to initialize __declspec(thread) or thread_local data in these runtime environments."

    This is enforced by compiler error C2482. In pure native code (as in the example), this restriction does not apply, but it illustrates that dynamic initialization of TLS objects is a known fragile area.

    1. Practical mitigations based on the documented behavior

    Given the above, and staying within the documented behavior:

    • Avoid heavy or heap-allocating constructors for thread_local globals that run at process or thread startup. Prefer:
      • A trivially constructible thread_local object plus an explicit init() that does heap work later, or
      • A thread_local pointer initialized to nullptr and lazily allocating on first use.
    • If the crash is related to thread-safe initialization overhead and TLS usage, and thread-safety is not required in the specific scenario, the documentation suggests disabling thread-safe initialization:
      • Compile with /Zc:threadSafeInit- to revert to non-thread-safe static initialization and avoid the UCRT-based TLS helper code:

        "To avoid taking a dependency on the UCRT, or to preserve the non-thread-safe initialization behavior of versions of Visual Studio prior to Visual Studio 2015, use the /Zc:threadSafeInit- option. If you know that thread-safety is not required, use this option to generate slightly smaller, faster code around static local declarations."

    • Be aware that this option affects static local initialization semantics globally; it should only be used when thread-safety for static locals is not needed.
    1. Summary
    • thread_local objects are implemented via TLS and initialized at thread startup; MSVC’s thread-safe initialization also uses TLS internally.
    • Heap allocation in a thread_local constructor during this phase can expose OS-level TLS or heap bugs or limits, leading to crashes on specific Windows builds.
    • Workarounds include restructuring initialization to avoid heap work in thread_local constructors and, where safe, disabling thread-safe initialization with /Zc:threadSafeInit-.

    References:

    AI-generated content may be incorrect. Read our transparency notes for more information.

    Was this answer helpful?

Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.