Does static initialization of a local variable always happens synchronized ?

26 views
Skip to first unread message

Bonita Montero

unread,
Sep 11, 2021, 5:06:41 AMSep 11
to
Look at this code:

#include <iostream>

using namespace std;

#if defined(_MSC_VER)
#define NOINLINE __declspec(noinline)
#elif defined(__GNUC__) || defined(__clang__)
#define NOINLINE __attribute((noinline))
#endif

struct S
{
S();
};

NOINLINE
S::S()
{
cout << "S::S()" << endl;
}

NOINLINE
S &fn()
{
static
S s;
return s;
}

int main()
{
fn();
fn();
}


I call fn twice, but of course s is initialized only once at the first
call of fn. What I asked myself if this happens synchronized, i.e. if
multiple threads try to initialize s, it's only initiailized only once
in a locked context.
With MSVC this happens as expected: before the initialization of s, fn
calls _Init_thread_header and afterwards it calls _Init_thread_footer.
These functions each call EnterCriticalSection or LeaveCriticalSection
on a global mutex. But is this behaviour guaranteed for C++ in general ?

Alf P. Steinbach

unread,
Sep 11, 2021, 7:47:56 AMSep 11
to
Yes.

C++ 17 §9.7/4 (next to last sentence applies):
❝Dynamic initialization of a block-scope variable with static storage
duration or thread storage duration is performed the first time control
passes through its declaration; such a variable is considered
initialized upon the completion of its initialization. If the
initialization exits by throwing an exception, the initialization is not
complete, so it will be tried again the next time control enters the
declaration. If control enters the declaration concurrently while the
variable is being initialized, the concurrent execution shall wait for
completion of the initialization. If control re-enters the declaration
recursively while the variable is being initialized, the behavior is
undefined.❞

- Alf

Paavo Helde

unread,
Sep 11, 2021, 8:14:08 AMSep 11
to
11.09.2021 12:06 Bonita Montero kirjutas:
> I call fn twice, but of course s is initialized only once at the first
> call of fn. What I asked myself if this happens synchronized, i.e. if
> multiple threads try to initialize s, it's only initiailized only once
> in a locked context.

Thread-safe initialization of statics is guaranteed since C++11.

Bonita Montero

unread,
Sep 11, 2021, 10:02:45 AMSep 11
to
And MSVC of course uses double-checked locking here !

Bonita Montero

unread,
Sep 11, 2021, 10:18:03 AMSep 11
to
It couldn't be really undefined since the flag which remembers that
the object has been created successfully is set afterwards. Otherwise
it woud be impossible that the object will be tried to be re-created
if the constructor throws an exception and the function containing
the object will be called again. So an infinite recursion is the only
possible behaviour here.

Alf P. Steinbach

unread,
Sep 11, 2021, 11:10:03 AMSep 11
to
The standard says it's undefined behavior, hence it's undefined
behavior. Really. So I believe that you meant to say that there can't be
any other actual effect of this UB than infinite recursion.

But of course there can be: formal UB means that the compiler is free to
insert checking that does whatever the compiler writers want.

By the way I'm sorry for not making clear that the thread safey
guarantee stems from C++11. Instead my quoting made it seem like C++17.
Happily Paavo Helde pointed that out else-thread. :)


- Alf

Bonita Montero

unread,
Sep 11, 2021, 11:18:52 AMSep 11
to
There isn't any other possibility.

> But of course there can be: formal UB means that the compiler is free
> to insert checking that does whatever the compiler writers want.

I didn't refer to the flag-checking but setting the flag. This can only
be done after creating the object since the constructor can throw an
exeption. But C++ shall try to re create the object at the next call;
so this flag can only be set afterwards.

David Brown

unread,
Sep 11, 2021, 2:04:11 PMSep 11
to
It is quite easy to see how you could get a deadlock from recursive use,
but not concurrent use. A typical initialisation sequence could be
roughly :

if (!initialised_flag) { // atomic flag
get_lock();
if (!initialised_flag) {
do_initialisation();
initialised_flag = true;
}
release_lock();
}

Calling this concurrently when the lock is taken, the second thread will
block on "get_lock()" until the first thread is finished, then see the
initialisation was done and go on its way. But calling it recursively,
the block at "get_lock()" will never finish because it is the same
thread that was supposed to be doing the initialisation.

On the other hand, the sequence could be:

if (!initialised_flag) {
initialise_temporary_object();
compare_and_swap(static_object, temporary_object, null);
initialised_flag = true;
}

That would be fine if there is no problem in accidentally creating extra
objects and discarding them. The compare and swap would avoid
overriding a previous initialisation. This would work fine with both
concurrent and recursive use.


So the standard says the behaviour is undefined on recursive use,
leaving the implementation freedom to pick either tactic (or other tactics).

Bonita Montero

unread,
Sep 11, 2021, 3:19:28 PMSep 11
to
Something I wondered about is whether it is possible to get a C function
pointer from a local static lambda _with_ captures like this:

#include <iostream>

int main()
{
int i;
static
auto lambda = [&i]()
{
++i;
};
void (*pLambda)() = lambda;
}

In theory this should be possible since the lambda doesn't need
its own context-pointer (pseudo-"this") since the capture-context
is global storage. This seems something the designers of C++ didn't
consider, although it is of little use.

Alf P. Steinbach

unread,
Sep 11, 2021, 4:05:23 PMSep 11
to
On 11 Sep 2021 21:19, Bonita Montero wrote:
> Something I wondered about is whether it is possible to get a C function
> pointer from a local static lambda _with_ captures like this:
>
> #include <iostream>
>
> int main()
> {
>     int i;
>     static
>     auto lambda = [&i]()
>     {
>         ++i;
>     };
>     void (*pLambda)() = lambda;
> }

This particular example — the lines of code up to the hypothetical
declaration of `pLambda` — is flawed because if you put this code in any
function other than `main` it will then use a dangling reference on
second call of that function.

---

> In theory this should be possible since the lambda doesn't need
> its own context-pointer (pseudo-"this") since the capture-context
> is global storage.

If you make `i` static then you have something that a smart enough
compiler can rewrite to a capture-less lambda. Don't know if they do
though. And conversion to C function pointer would need to be a language
extension.


---

> This seems something the designers of C++ didn't
> consider, although it is of little use.

You can get a freestanding function that references any C++ object
simply by storing a reference or pointer to (a copy of) that object
indirectly or directly in a static variable that the function uses.

For example, the type of the static variable can be `std::function<void()>`.

And that scheme can be elaborated to provide a function that you can use
like `as_c_callback( [&]{ ++i; } )`. To make that safe against multiple
overlapping uses of `as_c_callback` let the static variable be a
collection, e.g. with the return type of `as_c_callback` as a RAII
object with implicit conversion to C function pointer. To make it thread
safe use thread local storage. Uhm, not sure how to make it fool proof.
But if such elaborations are adopted it can probably be a good idea to
separate the elaborations from the basic simple scheme.

- Alf

Bonita Montero

unread,
Sep 12, 2021, 2:24:30 AMSep 12
to
Am 11.09.2021 um 22:05 schrieb Alf P. Steinbach:
> On 11 Sep 2021 21:19, Bonita Montero wrote:
>> Something I wondered about is whether it is possible to get a C function
>> pointer from a local static lambda _with_ captures like this:
>>
>> #include <iostream>
>>
>> int main()
>> {
>>      int i;
>>      static
>>      auto lambda = [&i]()
>>      {
>>          ++i;
>>      };
>>      void (*pLambda)() = lambda;
>> }
>
> This particular example — the lines of code up to the hypothetical
> declaration of `pLambda` — is flawed because if you put this code in
> any function other than `main` it will then use a dangling reference
> on second call of that function.

Of course there could be a dangling reference, but that's not what I'm
discussing.


> ---
>
>> In theory this should be possible since the lambda doesn't need
>> its own context-pointer (pseudo-"this") since the capture-context
>> is global storage.

> If you make `i` static then you have something that a smart enough
> compiler can rewrite to a capture-less lambda. Don't know if they do
> though. And conversion to C function pointer would need to be a language
> extension.

Of course I could make i static, but that's not what I'm talking about.

Reply all
Reply to author
Forward
0 new messages