Variáveis estáticas locais são tímidas

Caloni, 2018-02-20 computer blog

Uma dúvida muito comum dos programadores iniciantes em C/C++ diz respeito às variáveis static que são declaradas dentro de um escopo, como uma função. Sabemos que se ela fosse declarada global, fora de qualquer escopo, ela seria inicializada antes do main ser chamado, como diz este trecho de alguém que pesquisou a respeito:

"C++ Primer says. Each local static variable is initialized before the first time execution passes through the object's definition. Local statics are not destroyed when a function ends; they are destroyed when program terminates." - Someone that google it for but did not get it.

Mas no caso de variáveis static declaradas dentro de uma função isso não acontece, e ela pode ser inicializada a qualquer momento. Basta alguém chamar a função onde ela foi definida.

#include <iostream>

int func2()
{
    std::cout << "Func2 called\n";
    return 21;
}

int func()
{
    static int st_x = func2();
    return st_x * 2;
}

int main()
{
    std::cout << "Passing by...\n";
    std::cout << "Func returns " << func() << std::endl;
    std::cout << "Exiting...\n";
}
c:\Projects\caloni\projects>cl /EHsc static_local_sample.cpp
Microsoft (R) C/C++ Optimizing Compiler Version 19.12.25827 for x86
Copyright (C) Microsoft Corporation.  All rights reserved.

static_local_sample.cpp
Microsoft (R) Incremental Linker Version 14.12.25827.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:static_local_sample.exe
static_local_sample.obj

c:\Projects\caloni\projects>

c:\Projects\caloni\projects>static_local_sample.exe
Passing by...
Func2 called
Func returns 42
Exiting...

c:\Projects\caloni\projects>

Note que mesmo trocando static int para static const int a mesma coisa acontece. Apenas conseguimos forçar a inicialização antes do main quando há alguma variável global (static ou não) que chame a função.

#include <iostream>

int func2()
{
    std::cout << "Func2 called\n";
    return 21;
}

int func()
{
    static int st_x = func2();
    return st_x * 2;
}

static const int g_x = func();

int main()
{
    std::cout << "Passing by...\n";
    std::cout << "Func returns " << func() << std::endl;
    std::cout << "Exiting...\n";
}
c:\Projects\caloni\projects>cl /EHsc static_local_sample.cpp
Microsoft (R) C/C++ Optimizing Compiler Version 19.12.25827 for x86
Copyright (C) Microsoft Corporation.  All rights reserved.

static_local_sample.cpp
Microsoft (R) Incremental Linker Version 14.12.25827.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:static_local_sample.exe
static_local_sample.obj

c:\Projects\caloni\projects>static_local_sample.exe
Func2 called
Passing by...
Func returns 42
Exiting...

c:\Projects\caloni\projects>

O problema disso é que é possível que duas threads chamem func() "ao mesmo tempo", gerando uma dupla inicialização caso a implementação da libc não seja thread-safe. E a menos que o padrão especifique que essa inicialização deva ser thread safe, melhor fazer as coisas direito.

Mas, a título de curiosidade, é bom saber que no Visual Studio 2017 essa parte da libc já possui um mecanismo de proteção, como o sugestivo nome _tls_index já indica:

c:\Projects\caloni\projects>cl /EHsc /Zi static_local_sample.cpp
Microsoft (R) C/C++ Optimizing Compiler Version 19.12.25827 for x86
Copyright (C) Microsoft Corporation.  All rights reserved.

static_local_sample.cpp
Microsoft (R) Incremental Linker Version 14.12.25827.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:static_local_sample.exe
/debug
static_local_sample.obj

c:\Projects\caloni\projects>windbg static_local_sample.exe
static_local_sample!func [static_local_sample.cpp @ 10]:
   10 000fe410 push    ebp
   10 000fe411 mov     ebp,esp
   10 000fe413 push    0FFFFFFFFh
   10 000fe415 push    offset static_local_sample!wcschr+0x1cc7 (0018806c)
   10 000fe41a mov     eax,dword ptr fs:[00000000h]
   10 000fe420 push    eax
   10 000fe421 mov     eax,dword ptr [static_local_sample!__security_cookie
   (001a9080)]
   10 000fe426 xor     eax,ebp
   10 000fe428 push    eax
   10 000fe429 lea     eax,[ebp-0Ch]
   10 000fe42c mov     dword ptr fs:[00000000h],eax
   11 000fe432 mov     eax,dword ptr [static_local_sample!_tls_index
   (001ab51c)] ((( Thread Local Storage? )))
   11 000fe437 mov     ecx,dword ptr fs:[2Ch]
   11 000fe43e mov     edx,dword ptr [ecx+eax*4]
   11 000fe441 mov     eax,dword ptr [static_local_sample!st_x+0x4 (001ab028)]
   11 000fe446 cmp     eax,dword ptr [edx+104h]
   11 000fe44c jle     static_local_sample!func+0x79 (000fe489)
   ((( compara para ver se chama inicialização ou não )))

static_local_sample!func+0x3e [static_local_sample.cpp @ 11]:
   11 000fe44e push    offset static_local_sample!st_x+0x4 (001ab028)
   11 000fe453 call    static_local_sample!ILT+2810(__Init_thread_header)
   (000f1aff)
   11 000fe458 add     esp,4
   11 000fe45b cmp     dword ptr [
   static_local_sample!st_x+0x4 (001ab028)],0FFFFFFFFh
   11 000fe462 jne     static_local_sample!func+0x79 (000fe489)

static_local_sample!func+0x54 [static_local_sample.cpp @ 11]:
   11 000fe464 mov     dword ptr [ebp-4],0
   11 000fe46b call    static_local_sample!ILT+3110(?func2YAHXZ)
   (000f1c2b) ((( note a chamada a func2 )))
   11 000fe470 mov     dword ptr [static_local_sample!st_x (001ab024)],eax
   11 000fe475 mov     dword ptr [ebp-4],0FFFFFFFFh
   11 000fe47c push    offset static_local_sample!st_x+0x4 (001ab028)
   11 000fe481 call    static_local_sample!ILT+6165(__Init_thread_footer)
   (000f281a)
   11 000fe486 add     esp,4

static_local_sample!func+0x79 [static_local_sample.cpp @ 12]:
   12 000fe489 mov     eax,dword ptr [static_local_sample!st_x (001ab024)]
   ((( a partir da segunda chamada tudo começa aqui )))
   12 000fe48e shl     eax,1
   13 000fe490 mov     ecx,dword ptr [ebp-0Ch]
   13 000fe493 mov     dword ptr fs:[0],ecx
   13 000fe49a pop     ecx
   13 000fe49b mov     esp,ebp
   13 000fe49d pop     ebp
   13 000fe49e ret
[comment] [Contra o array de 100 bytes]