Showing posts with label cpp_const_initilaize. Show all posts
Showing posts with label cpp_const_initilaize. Show all posts

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();
}

Dec 8, 2022

[C++/Rust] use of thread_local in code.

Reference:
  1. All about thread-local storage by MaskRay
  2. A Deep dive into (implicit) Thread Local Storage; in detail about use cases for thread_local.
  3. ELF Handling For Thread-Local Storage by Ulrich Drepper
  4. clang attribute 'tls-model'
  5. How fast is thread local variable access on Linux
  6. Mastering x86 Memory Segmentation
  7. x86 and amd64 instruction reference

This note is focused on C++ coding practice with thread_local; knowledge are collected from daily engineering and references above.


C++ Language definitions:
  1. Zero-initialization
    https://en.cppreference.com/w/cpp/language/zero_initialization
    https://vsdmars.blogspot.com/2014/04/c11-zero-initialisation-for-classes.html
  2. Constant initialization
    https://en.cppreference.com/w/cpp/language/constant_initialization
    Init. Rule memorize: C.Z , Constant first if possible, then Zero init.
  3. constinit specifier
    https://en.cppreference.com/w/cpp/language/constinit
  4. Potentially-evaluated expressions
    https://en.cppreference.com/w/cpp/language/expressions#Potentially-evaluated_expressions
  5. [C++20] consteval / constexpr
    https://vsdmars.blogspot.com/2022/06/cc20-consteval-constexpr.html


  6. The thread_local keyword is only allowed for objects declared at namespace scope, objects declared at block scope, and static data members.
    It indicates that the object has thread storage duration.
    If thread_local is the only storage class specifier applied to a block scope variable, static is also implied.
    It can be combined with static or extern to specify internal or external linkage (except for static data members which always have external linkage) respectively.
    It can be combined with constinit to reduce overhead that would otherwise be incurred by a hidden guard variable. (i.e. static is thread safe guarded)
  7. thread storage duration. The storage for the object is allocated when the thread begins and deallocated when the thread ends. Each thread has its own instance of the object(i.e. clone()). Only objects declared thread_local have this storage duration. thread_local can appear together with static or extern to adjust linkage.
  8. thread_local is init. ordered in C.Z; i.e first init. with const-init; if can't, do zero-init.
When variable decorated with static or thread_local it will be constant initialized if possible than a runtime zero initialization. [[basic.start.static]]
i.e.
#include <iostream>
using namespace std;

bool runtimeFunc() {
  return std::is_constant_evaluated(); // always false
}

constexpr bool constexprFunc() {
  return std::is_constant_evaluated(); // may be false or true
}

consteval bool constevalFunc() {
  return std::is_constant_evaluated(); // always true
}

void foo() {
  static bool v1 = constexprFunc();       // T

  // implicit static
  thread_local bool v2 = constexprFunc(); // T
  thread_local bool v3 = constevalFunc(); // T

  int y = 42;
  static int v4 = y + runtimeFunc();         // 42
  static int v5 = y + constexprFunc();       // 42
  static int v6 = y + constevalFunc();       // 43

  // implicit static
  thread_local int v7 = y + runtimeFunc();   // 42
  thread_local int v8 = y + constexprFunc(); // 42
  thread_local int v9 = y + constevalFunc(); // 43
}

int main() { foo(); }

Usage
  • thread_local should not be used in signal handler; while signal handler can be called in different threads, thus the fact that thread_local is not sync between threads can introduce buggy logic.
  • thread_local is relatively slow in DSO use cases, use local caching instead.
        1 instruction in Windows, Linux
        3-4 in OSX
  • in dlopen; DSO interacts with thread_local as follows:
    • When a thread starts(i.e. clone()), init. thread_local objects with thread storage duration at namespace scope.
      When a thread exits, destruct objects with thread storage duration.
  • What happens if the library is unloaded before all threads exit?
    • In glibc, use RTLD_NODELETE, this will have DF_1_NODELETE set in ELF, thus does not unload the shared object during dlclose().
    • Consequently, the object's static and global variables are not reinitialized if the object is reloaded with dlopen() at a later time.
    • Also,  dlclose() in the middle of destructing thread_local objects is a no-op when RTLD_NODELETE is used.
    • Use cases for thread_local in DSO can be slow due to __tls_get_addr@plt to get the address of the thread_local variable out of the DSO.
  • Thread local variables should not be used in coroutines to prevent buggy logic.
    https://rules.sonarsource.com/cpp/RSPEC-6367
    If you have to use thread local inside a signal handler function, read:
    https://vsdmars.blogspot.com/2025/11/c-avoid-compiler-reordering-statements.html (std::atomic_signal_fence)