In general, function-scope static variables are initialized when control first reaches the variable declaration (e.g. when the the function is first called), but under some circumstances they can be initialized early. In what circumstances can we guarantee that the constructor of a static function-scope variable is not called before the function is first called?
Specifically, I have an example like this:
(On godbolt)
// a.h
void initA();
void setA(int val);
int getA();
// a.cpp
#include "a.h"
static int a;
void initA() {
a = 1;
}
void setA(int val) {
a = val;
}
int getA() {
return a;
}
// b.h
class B {
public:
B();
};
// b.cpp
#include "b.h"
#include "a.h"
B::B() {
setA(12);
}
// main.cpp
#include "a.h"
#include "b.h"
#include <iostream>
void local_statics();
int main() {
initA(); // sets a static namespace-scope int `a` to 1
local_statics();
std::cout << getA() << std::endl; // Is this guaranteed to print 12?
}
void local_statics() {
// Constructor of B sets a static namespace-scope int `a` to 12
static B b;
}
Does the standard guarantee that a will be set to 12?
b is not being zero-initialized or constant-initialized (since the constructor is not constexpr), so according to the standard in generals it will be initialized the first time control passes through its declaration (that is, when the function is called), in which case a will have the value 12 as expected:
The zero-initialization (8.5) of all block-scope variables with static storage duration (3.7.1) or thread storage duration (3.7.2) is performed before any other initialization takes place. Constant initialization (3.6.2) of a block-scope entity with static storage duration, if applicable, is performed before its block is first entered. An implementation is permitted to perform early initialization of other block-scope variables with static or thread storage duration under the same conditions that an implementation is permitted to statically initialize a variable with static or thread storage duration in namespace scope (3.6.2). Otherwise such a variable is initialized the first time control passes through its declaration; such a variable is considered initialized upon the completion of its initialization
However, I'm a little unclear on the circumstances when "An implementation is permitted to perform early initialization of other block-scope variables... under the same conditions that an implementation is permitted to statically initialize a variable with static or thread storage duration in namespace scope (3.6.2)". The conditions listed in section 3.6.2 are:
An implementation is permitted to perform the initialization of a non-local variable with static storage duration as a static initialization even if such initialization is not required to be done statically, provided that
- the dynamic version of the initialization does not change the value of any other object of namespace scope prior to its initialization, and
- the static version of the initialization produces the same value in the initialized variable as would be produced by the dynamic initialization if all variables not required to be initialized statically were initialized dynamically.
I just don't quite understand these two conditions and exactly how they would apply.
The first block of wording that you are quoting has been replaced for C++17 and as a defect report against previous C++ revisions via CWG 2026.
The wording is now clearer and[stmt.dcl]/3 now only says:
Dynamic initialization of a block variable with static storage duration or thread storage duration is performed the first time control passes through its declaration; [...]
[basic.start] has been modified to handle the static initialization for local static storage duration variables in the same way as for non-local ones.
This means that the initialization behavior is now the same for local and non-local static storage duration variables, except that if dynamic initialization of a local variable happens, then it happens when control passes first through its definition (with special rules on exceptions and thread-safety).
The second quoted block is still in the standard except that the "non-local" qualifier has been removed.
The compiler is permitted to perform static initialization for b here:
The first bullet is satisfied, because in the dynamic version a is only modified after its static initialization.
The second bullet is satisfied, because the value produced in B is always the same trivially as the type B has no state.
When the compiler performs static initialization of b, then the program will print 1.
That this ignores the different side effects resulting from the initialization choice is a known unresolved defect in the standard, tracked in the open CWG issue 1294.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With