atum
atum copied to clipboard
Helpers for preventing the static initialization order fiasco of global variables.
atum
Like Atum, the Egyptian god of pre-existence and post-existence, this single-header library takes care of global variable initialization and destruction. Normally, global initialization between translation units is done in an unspecified order, which makes it problematic to access global variables in the constructors of other global variables. This is referred to as the static initialization order fiasco and atum has multiple solutions.
#include <atum.hpp>
// A global that is initialized at compile time.
ATUM_CONSTINIT std::mutex mutex;
// A global that is initialized manually.
ATUM_CONSTINIT atum::manual_init<std::string, atum::init_braced<'a', 'b', 'c'>> string;
// A global that is initialized on demand.
ATUM_CONSTINIT atum::lazy_init<struct container, std::vector<int>> container;
int main()
{
atum::scoped_initializer<string> initializer;
container->push_back(42);
std::cout << "string " << *string << '\n';
}
Features
- single-header, C++17 library with minimal standard library dependencies
- forward-compatible macro for C++20's
constinitkeyword - manual, lazy, and nifty counter initialization
- optional debug mode that checks global variable lifetime before access
Documentation
Installation
Just copy the single header include/atum.hpp into your project and enable the C++17 compiler flag.
Alternatively you can use the CMake target foonathan::atum either via subdirectory or installation.
Lifetime checking is enabled by defining ATUM_CHECK_LIFETIME to 1 (defaults to no checking).
The library will then keep track whether a global has been initialized and error on access of an uninitialized object.
The assertion can be customized by further defining an ATUM_LIFETIME_ASSERT(Cond) macro, which defaults to assert(Cond).
Init classes
The classes ending with _init all have an interface that looks like this:
/// Holds an object of the specified type and will initialize it using the `Initializer`.
template <typename T, typename Initializer = atum::init_default>
class Init
{
public:
using element_type = T;
//=== construction ===//
/// constexpr default constructor that does nothing.
constexpr Init();
// Initialize the object using the `Initializer`.
// This might be called automatically, or you have to manually call it depending on the semantics.
void initialize();
// Destroy the object.
// This might be called automatically, or you have to manually call it depending on the semantics.
void destroy() &&;
//=== access ===//
element_type& get();
element_type& operator*();
element_type* operator->();
};
They hold an object of the specified type and provide pointer-like access as well as a .get() method.
As they do not initialize anything in their constructors, the Init objects themselves are subject to constant initialization and thus always available.
Initialization is done using an Initializer, which controls how the object is initialized:
atum::init_default: initialize using the default constructor. This is the default if no initializer is specified.atum::init_from<args...>: initialize usingT(args...). Note that the arguments are passed as template parameters.atum::init_braced<args...>: initialize usingT{args...}. Note that the arguments are passed as template parameters.atum::init_fn<Fn>: initialize usingT(Fn()), whereFnis some function pointer.
The helper class atum::scoped_initializer<Inits...> takes some Init objects as template parameter and will call .initialize() on all of them in its constructor and .destroy() in its destructor (in reverse order).
It can be used to initialize atum::manual_init variables, but is also valid for all other Init classes.
If used with other Init classes, it might have no effect, but will definitely ensure that the global is initialized while the atum::scoped_initializer object lives.
ATUM_CONSTINIT atum::manual_init<std::string> a;
ATUM_CONSTINIT atum::manual_init<std::string> b;
ATUM_CONSTINIT atum::lazy_init<std::string> c;
int main()
{
atum::scoped_initializer<a, b, c> initializer;
// During this scope, a, b, c are initialized.
// c was going to be initialized on access anyway, but the initializer has done it earlier during its constructor.
}
Constant Initialization
ATUM_CONSTINIT int i = 42; // constant
ATUM_CONSTINIT std::mutex mutex; // constexpr default constructor
The best way of initializing global variables is using constant initialization (performed at compile time) and should be done whenever possible.
Use the ATUM_CONSTINIT macro to get a compiler error if constant initialization could not be performed at compile-time (only with C++20 or on supported compilers).
Manual Initialization
template <typename T, typename Initializer = atum::init_default>
class manual_init
{
// See `Init` for interface.
};
Use the Init class atum::manual_init to declare a global variable that has to be initialized manually.
Calling .initialize() will initialize it, and calling .destroy() will destroy it.
You have to manually ensure that the globals are initialized before their first use and in the correct order to handle inter-global dependencies.
The recommended way is to perform initialization in the beginning of main() and destruction at the end.
// Some global logger.
ATUM_CONSTINIT atum::manual_init<Logger> logger;
// Some other global that logs something in its constructor.
ATUM_CONSTINIT atum::manual_init<Global> global;
int main()
{
// We need to ensure that `logger` is initialized before `global`.
atum::scoped_initializer<logger, global> initializer;
// You can now access the variables.
logger->log(*global);
}
Lazy Initialization
template <typename Tag, typename T, typename Initializer = atum::init_default>
class lazy_init
{
// See `Init` for interface.
};
Use the Init class atum::lazy_init to declare a global variable that is initialized on first use or when .initialize() is called (whatever happens first).
The lazy initialization is done in a thread-safe way by leveraging a function local static.
The Tag argument is some type that just has to be different for each atum::lazy_init object.
Note: Due to the fact that destruction of globals is done in a LIFO order that might be undesirable, .destroy() is a no-op, i.e. a lazily initialized global is never destroyed.
This can create resource leaks.
// A logger that is initialized on first use.
// The tag type ensures that we can create multiple global loggers that are distinct objects nonetheless.
ATUM_CONSTINIT atum::lazy_init<struct logger, Logger> logger;
int main()
{
// First usage will create logger.
logger->log("Hello!");
}
Nifty Initialization
template <typename T, typename Initializer = atum::init_default>
class nifty_init
{
public:
// Provides compile-time, unchecked access to the stored object.
constexpr T& reference() const noexcept;
// See `Init` for remaining interface.
};
template <auto& Nifty>
using nifty_counter_for = scoped_initializer<Nifty>;
Use the Init class atum::nifty_init to declare a global variable that is initialized using nifty counters.
It is very similar to atum::manual_init but uses a counter to allow calling .initialize() and .destroy() multiple times:
only the first call to .initialize() and last call to .destroy() will actually initialize/destroy the object; the others have no effect.
Nifty counters allow initialization that happens automatically before use, just like with atum::lazy_init, but also ensure destruction.
It is used by std::cout, for example.
The header for the global variable must create three objects:
- The
atum::nifty_initobject that will be initialized at compile-time. - A
staticatum::nfity_counter_forobject that will have copies for each translation-unit the header is included. It ensures the automatic initialization. - A reference to the object stored in the init object; users can just access it directly and it will have been initialized.
// logger.hpp
// A logger that is initialized using nifty counters.
inline ATUM_CONSTINIT atum::nifty_init<Logger> logger_init; // The init object.
static atum::nifty_counter_for<logger_init> logger_counter; // The per-translation-unit counter object.
inline ATUM_CONSTINIT Logger& logger = logger_init.reference(); // The global users are going to access.
// user.cpp
#include "logger.hpp"
int main()
{
// Just use the reference.
logger.log("Hello World!");
}
Note: for nifty initialization to work properly, a couple of things have to be insured:
-
The header of every global
Bthat depends on a nifty initialized globalAmust include the header ofA- even if theAis otherwise an implementation detail ofB. Otherwise, users ofBwill not get thestaticnifty counter object that will initializeA.// bad: class Global { public: // constructor uses nifty-initialized Logger Global(); }; // good: #include "logger.hpp" class Global { public: // constructor uses nifty-initialized Logger Global(); }; -
If you're using a nifty initialized global
Ain the constructor of some variable template orstaticdata member of a template, it is not guaranteed to work.class Global { public: // constructor uses nifty-initialized Logger Global(); }; template <typename T> struct Template { static Global global; // dangerous };In that case you need to create an
atum::nfity_counter_forobject yourself. -
Some combination of dynamically linked libraries is probably also problematic.