May 15, 2025

[C++] global const variable initialization

Reference:

[C++][C++20] consteval / constexpr
[C++/Rust] use of thread_local in code
[Note] linking notes


Initializing order

  1. Perform constant initialization and zero initialization
  2. Perform dynamic initialization
  3. Start executing main()  How main() is executed on Linux
  4. Perform dynamic initialization of function-local static variables, as needed.
  5. It is impossible to read uninitialized global memory.
  6. Destroy function-local static variables that were initialized in reverse order
  7. Destroy other globals in reverse order.

header.h
inline int global = compute();
// Always OK due to sequence; and `h` is inline.
// (compiler will take care of its init.)
inline int h = global;

tu.cpp
#include "header.h"
int a = global; // not certain due to TU not guarantee `global` being defined.

Templated variables are init. at some point before main()

template<typename T>
int type_id = get_id<T>();

template<typename T>
struct templ {
 static int type_id = get_id<T>();
};

Compiler is allowed to perform dynamic initialization at compile-time.

int f(int i) {
  return 2 * i; // not constexpr
}

int the_answer = f(21); // might happen at compile-time; even not constexpr.

Compiler is allowed to perform dynamic init. after main() has started executing.

i.e. an unused global variable might not be initialized at all.

const auto start_time = std::chrono::system_clock::now();

int main() {
  auto t = start_time; // might be init. at this point.
}

The static initialization order fiasco

Dynamic initialization order of globals in different translation units is not specified.

Guideline
Unless otherwise allowed by its documentation, do not access global state in the constructor of another global.

* Solution 1

Globals initialized by some constant expression are initialized during static initialization without dynamic initialization.
Constant expressions cannot access globals that are dynamically initialized.

Guideline
Whenever possible, make global variables constexpr.

constexpr float pi = 3.14;
constexpr std::size_t threshold = 3;

Guideline
Whenever possible, use constant initialization.

// https://en.cppreference.com/w/cpp/thread/mutex/mutex
std::mutex mutex; // Has constexpr constructor. 


Always look into the code path that has function which is not constexpr.

e.g.
constexpr int compute(bool default) {
  if(default)
    return 42; // constexpr
  else
    return std::getchar(); // runtime.
}

int default = compute(true); // constexpr
int non_default = compute(false); // runtime

C++20

constinit = constexpr - const

  • variable is initialized using constant initialization (or error)
  • variable is not const
thus:
constinit std::mutex mutex; // OK.

constinit int default = compute(true); // constexpr; ok.
constinit int non_default = compute(false); // runtime, thus error.

Guideline
Declare global variables constinit to check for initialization problems.

Guideline
Try to add a constexpr constructor to every type.
e.g.
Default constructors of containers(with default allocator)
Default constructors of resource handles(files, sockets, ...)

Guideline
Do not use constinit if need to do lazy initialization.

* Solution 2:

Lazy initialization.

Global& global() {
  static Global g;
  return g;
}

// The global init. is not determinated.
Global& global = global(); // Bad because can't be constinit

Guideline
Never cache a (reference to) a function-local static in a global variable.
template<typename Tag, typename T>
class lazy_init {
  public:
    constexpr lazy_init() = default;

    T& get() {
      static T global;
      return global;
    }

    T& operator*() { return get();}
    T* operator->() { return &get();}
};

constinit lazy_init<TAG, Logger> global; // OK
global->log(); // The first call takes time, beware.
logger.h
class Logger{...};

extern constinit lazy_init<TAG, Logger> global;
logger.cpp
constinit lazy_init<TAG, Logger> global;

Global variable destruction

Global variables are destroyed in reverse dynamic initialization order.
Beware, constinit always initialized first, and that is its purpose.
A a;
constinit B b;

void func(){
  static C c;
}

int main() {
  func();
}
init. b
init. a
init. c
destroy c
destroy b
destroy a


Destruction order of globals in different TUs is not specified.

Guideline
Unless otherwise allowed by its documentation, do not access global state in the destructor of another global.
This applies to constinit globals.

Rule
  • The order of dynamic initialization is not specified.
  • Exception: within one translation unit, variables are initialized from top to bottom.
    1) Must include the header that declares the global before using it
    2) every global defined in that header is initialized before your global.
  • Do not use function-local static variables if there's a chance they might be used after main()
  • Do not use function-local static variables.

template<typename Tag, typename T>
class lazy_init {
  public:
    constexpr lazy_init() = default;

    T& get() {
    // technique; 1) thread safe, 2) initialize data member in one-shot.
      static bool dummy = (storage_.initialize(), true);
      return storage_.get();
    }
  private:
    storage<T> storage_;
};

constinit lazy_init<struct global_tag, std::string> global;

Nifty counters

  • Ensure the nifty counter object is included in the definition file.
  • The nifty counter approach doesn't work for templated objects.

header.h
extern constinit nifty_init<Global> global_init; // declaration; definition is in some .cpp file.
// static int dummy = (global_init.initialize(), 0);
static nifty_counter_for<global_init> dummy; // definition
inline constinit Global& global = global_init.reference();

constinit Global copy = global; // compiler error
a.cpp
#include "header.h"
int a = global->foo();
b.cpp
#include "header.h"
int b = global->foo();

template<typename T>
class nifty_init {
public:
 constexpr nifty_init() = default;
 
 void initialize() {
 	if (counter_++ == 0)
		storage_.initialize();
 }
 
 void destroy() {
	if (--counter_ == 0)
		storage_.get().~T();
 }
 
 constexpr T& reference() { return storage_.get(); }

private:
 int counter_ = 0;
 storage<T> storage_;
};


template<auto& NiftyInitT>
struct nifty_counter_for {
 
  nifty_counter_for() {
  	NiftyInitT.initialize();
  }
  
  ~nifty_counter_for() {
    NiftyInitT.destroy();
  }
};

Conclusion

  • constinit is not always applicable
  • lazy initialization has to leak
  • nifty counters are black magic
  • Or, simply don't do anything before or after main()


Manual initialization (Google's way)

Guideline
Use manual initialization either for all globals in your project, or none.
And remember to initialize them all!


header.h
template<typename T>
class manual_init {
public:
  constexpr manual_init() = default;
  void initialize() { storage_.initialize(); }
  void destroy() { storage_.destroy(); }
  
  T& get() { return storage_.get(); }
  
private:
 storage<T> storage_;
};


template<auto& ... Init>
struct scoped_initializer {
  scoped_initializer() {
    Init.initialize();
	...
  }
  
  ~scoped_initializer() {
    Init.destroy();
	...
  }
};

main.cpp
#include "header.h"

constinit manual_init<Global> global;

int main() {
  scoped_initializer<global> initializer;
  global->foo();
}

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.