C++20 constinit specifier
In this three part tutorial series, we explore the new specifier, constexpr
specifier that is available since C++11.
In the first part I explain the constinit
specifier. The most simple explaination of constinit
is that it guarantees that the variable is initialized at compile time and when the initialization is not possible we get a compilation error.
1. What does constinit mean?
constinit
specifier can be used only with static storage duration variables to and it guarantees compile time initialization of variables. Lets see this in action. To demonstrate the examples, it would be good to know how to identify when a variable has a static storage duration.
const int gConstInt{9}; //const global variable has static storage
static int gStaticInt{9}; //global variables with static specifier
//are same as gConstInt
static const int gStaticConstInt{9}; //static and const together on
// a global variable is trivial
int gInt{9}; //otherwise global variables have automatic
//storage duration
const int gConstIntDefault; //compilation error since const
//variables cannot be uninitialzed
void localMethod(){
int localVar1{9}; //local variables have automatic storage
int localVar2; //duration too
}
In the above cases, apart from the cases where const
qualifier appears before a variable, the variables can be modified later during compile or runtime context.
int main(){
gConstInt = 8; // compilation error
gStaticInt = 8; //Ok
gStaticConstInt = 8; //compilation error
gInt = 8; //Ok
constinit int lConstinitInt{9}; //compilation
}
However the static storage duration variables declared above doesn’t guarantee that the variables are initialized (except for the case where const
— qualifier appears before the variable). To have such a guarantee, constinit
specifier can be used. constinit
, however doesn’t imply that the variables are constant. Variables with constinit
specifier can be changed at run- or compile-time contexts
int foo(){return 9;}
constexpr int bar(){return 9;}
constinit int gConstinitInt1{9};
constinit const int gConstinitConstInt{9};
constinit int gConstinitInt2 = foo(); //compilation error since foo
//is not constexpr
constinit int gConstinitInt3 = bar(); //Ok since bar is constexpr
int localMethod2(){
constinit localVar3{9}; //compilation error, local variables
//are not static storage and hence
//cannot be constinit
static constinit int localVar4{9}; //okay since static keyword
//used to specify static
//storage for local var
}
int main(){
gConstinitInt = 8; //Ok
gConstinitConstInt = 8; //compilation error
}
2. What is the difference constinit and constexpr?
constexpr
implies constinit
but vice-versa is not true. constexpr
variables are required to be initialized at compile time just like constinit
variables but behave like compile-time constants and cannot be changed later, unlike constinit
variables
constexpr int gConstexprInitialized{9}; //okay
constexpr int gConstexprUninitialized; //compilation error
constinit int gConstinitInt{9};
int main(){
gConstexprInitialized = 8; //compilation error: constexpr
//variables cannot be modified
gConstInitInt = 8; //Ok
}
Moreover the compile time evaluated context where constexpr
variables can be used doesn’t apply to constinit
variables. For e.g.:
#include <array>
constexpr std::size_t getArraySizeBasedOnArchitecture(){
return sizeof(std::size_t*100);
}
constinit auto arrSize2 = getArraySizeBasedOnArchitecture();
constexpr auto arrSize1 = getArraySizeBasedOnArchitecture();
int main(){
std::array<int, arrSize1> = intArray1; //Ok
std::array<int, arrSize2> intArray2; //compilation error
}
Also, if one takes a closer look on the assembly code for constexpr
and constinit
variables, one can easily see that variables with former have no machine code while the latter has. This can be seen in the image below taken from compiler explorer where the variable var1
having constinit
specifier has a corresponding assembly code whereas the var2
doesn’t have any corresponding assembly code.
The illustration above doesn’t imply that constinit
is inferior to constexpr
but it shows the subtle differences that might not be obvious at first glance. Both have their use (as already stated constinit
variables can be modified at runtime whereas constexpr
variables cannot) depending on the context (compile time vs runtime) we want to program.
Lastly, it is useful to remember that both constexpr
and constinit
cannot appear together on a variable. It results in compilation error. However, as shown in code snippets, constinit
can appear with static
(makes sense only in local scope variables) and const
qualifier.
3. Where can it be useful ?
Apart from the obvious observations to guarantee that variables are initialized at compile time and that compiler generates error when the variables cannot be initialized at compile time, constinit
is very useful to overcome the challenges related to static initialization order fiasco. For more details look at this blog.
4. Summary
A short summary of main points form this article are below:
constinit
specifier applies only to variablesconstinit
guarantees that variables are initialized during compilation, else we get a compilation error.constinit
specifier implies static storage duration however the reverse is not trueconstexpr
variables implyconstinit
, however the reverse is not true.- Only one of
constexpr
andconstinit
specifier can appear on a variable. constinit
can be applied toconst
— qualified variables.
5. Conclusion
Prefer constinit
via constinit for both constant and non-constant variables whenever the variables are expected or required to be initialized during compilation. Whenever the variables are not initialized at compile time, the compiler throws an error.
In the next article in the series, I’ll explain the usage of consteval
specifier. and its difference and similarities to constexpr
specifier. Stay tuned!