This is an unofficial snapshot of the ISO/IEC JTC1 SC22 WG21 Core Issues List revision 113d. See http://www.open-std.org/jtc1/sc22/wg21/ for the official list.

2024-03-20


2429. Initialization of thread_local variables referenced by lambdas

Section: 8.8  [stmt.dcl]     Status: C++20     Submitter: Princeton Ferro     Date: 2019-08-22

[Adopted as a DR at the November, 2019 meeting.]

According to 6.7.5.3 [basic.stc.thread] paragraph 2,

A variable with thread storage duration shall be initialized before its first odr-use (6.3 [basic.def.odr]) and, if constructed, shall be destroyed on thread exit.

According to 8.8 [stmt.dcl] paragraph 4, for block-scope variables this initialization is peformed when the declaration statement is executed:

Dynamic initialization of a block-scope variable with static storage duration (6.7.5.2 [basic.stc.static]) or thread storage duration (6.7.5.3 [basic.stc.thread]) is performed the first time control passes through its declaration; such a variable is considered initialized upon the completion of its initialization.

However, there are cases in which the flow of control does not pass through a variable's declaration, leaving it uninitialized in spite of it being odr-used. For example:

  #include <thread>
  #include <iostream>

  struct Object {
    int i;
    Object() : i(3) {}
  };

  int main(void) {
    static thread_local Object o;

    std::cout << "[main] o.i = " << o.i << std::endl;
    std::thread([] {
      std::cout << "[new thread] o.i = " << o.i << std::endl;
    }).join();
  }

o is a block-scope variable with thread storage and a dynamic initializer. The lambda passed into std::thread's constructor refers to o but does not capture it, which should be fine since o is not a variable with automatic storage. However, when control passes through the lambda, it will do so on a new thread. When that happens, it will refer to o for the first time. Because o is a thread-local variable, it should be initialized, but because it is declared in block scope, it will only be initialized when control passes through its declaration, which will never happen on the new thread.

This example is straightforward, but others are more ambiguous:

  #include <thread>
  #include <iostream>

  struct Object {
    int i;
    Object(int v) : i(3 + v) {}
  };

  int main(void) {
    int w = 4;
    static thread_local Object o(w);

    std::cout << "[main] o.i = " << o.i << std::endl;
    std::thread([] {
      std::cout << "[new thread] o.i = " << o.i << std::endl;
    }).join();
  }

Here the initialization of o uses the value of w, which is not captured. Perhaps it should be ill-formed for a lambda to refer to a block-scope thread-local variable.

Proposed resolution (October, 2019):

Change 6.7.5.3 [basic.stc.thread] paragraphs 1 and 2 as follows:

All variables declared with the thread_local keyword have thread storage duration. The storage for these entities shall last lasts for the duration of the thread in which they are created. There is a distinct object or reference per thread, and use of the declared name refers to the entity associated with the current thread.

[Note: A variable with thread storage duration shall be is initialized before its first odr-use (6.3 [basic.def.odr]) as specified in 6.9.3.2 [basic.start.static], 6.9.3.3 [basic.start.dynamic], and 8.8 [stmt.dcl] and, if constructed, shall be is destroyed on thread exit (6.9.3.4 [basic.start.term]). end note]