What Is 'Constinit' in C++20

What is `constinit` in C++20?

  • What does constinit mean? Why was it introduced? In which cases should we use it?

Initializing a variable with static storage duration might result in two outcomes¹:

  1. The variable is initialized at compile-time (constant-initialization);

  2. The variable is initialized the first time control passes through its declaration.

Case (2) is problematic because it can lead to the static initialization order fiasco, which is a source of dangerous bugs related to global objects.

The constinit keyword can only be applied on variables with static storage duration. If the decorated variable is not initialized at compile-time, the program is ill-formed (i.e. does not compile).

Using constinit ensures that the variable is initialized at compile-time, and that the static initialization order fiasco cannot take place.


  • Does it make a variable immutable? Does it imply const or constexpr?

No and no.

However, constexpr does imply constinit.


  • Can a variable be both const and constinit? What about constexpr and constinit?

It can be both const and constinit. It cannot be both constexpr and constinit. From the wording:

At most one of the constexpr, consteval, and constinit keywords shall appear in a decl-specifier-seq.

constexpr is not equivalent to const constinit, as the former mandates constant destruction, while the latter doesn't.


  • To which variables can the specifier be applied? Why cannot we apply it to non-static, non-thread_local variables?

It can only be applied to variables with static or thread storage duration. It does not make sense to apply it to other variables, as constinit is all about static initialization.


  • Does it have any performance advantages?

No. However, a collateral benefit of initializing a variable at compile-time is that it doesn't take instructions to initialize during program execution. constinit helps developers ensure that is the case without having to guess or check the generated assembly.


¹: See https://en.cppreference.com/w/cpp/language/storage_duration#Static_local_variables

What's the real difference between constinit and constexpr ?

A constinit variable is constant initialized, but it is not usable in a constant expression, nor even automatically constant. Your main can legally contain this line

c2 = 2; 

Yup, modification is possible after initialization.

Can C++20 `constinit` waive the need for nifty counter idiom?

Can C++20 constinit waive the need for nifty counter idiom?

No.

Static initialisation order fiasco is only a problem with dynamic initialisation phase of static objects. Sure, if you don't do dynamic initialisation, then there is no problem, and constinit enforces that. But that doesn't solve anything when you need dynamic initialisation.

C++20 consteval functions and constexpr variables - are they guaranteed to be evaluated at compilation time?

The standard does not have the concept of "compile time". There is only "constant evaluation" and a "constant expression". Implementations may implement "constant expressions" such that they are evaluated at compile time.

But there is no C++ standard-defined behavior you can point to in the actual C++ program that makes a "constant expression" equivalent to compile-time compilation. It is 100% legal for a compiler to emit code for constant expressions, and it always has been.

From [expr.const]/13:

"An immediate invocation [ie: calling a consteval function] shall be a constant expression

So calling a consteval function is a constant expression. Does this mean that the compiler certainly will not emit actual assembly for this function that gets called at runtime? No; that's a matter of the quality of the implementation.

However, certain uses of a constant expression do inform various compiler decisions. This means that, while the compiler does not in general have to evaluate them at compile time, if the value is used in a place that affects the basic act of compilation, the compiler must in that instance evaluate the expression right then.

For example:

std::is_same_v<array<int, some_constexpr_func(3, 4)>, array<int, 7>>

Whether this (constant) expression evaluates to true or false depends on what some_constexpr_func(3, 4) returns. If it returns the sum of its parameters, it is true. And the compiler needs to know that, because it needs to emit the code for the std::array<int, S> type, which as part of its type includes its template parameters. So it needs to know what they are.

Outside of cases where constant evaluation affects compiler decisions, whether any constant evaluation happens at compile time is up to the quality of your compiler.

Are static constinit member variables identical to non-type template parameters?

constinit has exactly and only one semantic: the expression used to initialize the variable must be a constant expression.

That's it. That's all it does: it causes a compile error if the initializing expression isn't a valid constant expression. In every other way, such a variable is identical to not having constinit there. Indeed, the early versions of the proposal had constinit as an attribute rather than a keyword, since it didn't really do anything. It only gave a compile error for invalid initialization of the variable.

For this case, there is no reason not to make the variable constexpr. You clearly don't intend to change the variable, so there's no point in making the variable modifiable.

Is east constexpr / constinit / consteval allowed in C++20?

Sort of.

It's part of the decl-specifier-seq, and the specifiers in that can be in any order. It's the same rule that allows you to write volatile int static long unsigned inline long const x = 1;

But it isn't part of the declarator (in particular, the ptr-operator), so you can't do int* constexpr x = nullptr;.

Using constinit variable to initialize a constexpr variable

Yes, the diagnostic is correct. constexpr variables must be initialized with a constant expression, and a is not a constant expression (it's a mutable variable).

The purpose of constinit (P1143) is to force a variable declaration to be ill-formed if it's initialization is not constant. It doesn't change anything about the variable itself, like it's type or anything (in the way that constexpr is implicitly const). Silly example:

struct T {
int i;
constexpr T(int i) : i(i) { }
T(char c) : i(c) { }
};

constinit T c(42); // ok
constinit T d('X'); // ill-formed

That is all constinit is for, and the only real rule is [dcl.constinit]/2:

If a variable declared with the constinit specifier has dynamic initialization ([basic.start.dynamic]), the program is ill-formed.
[ Note: The constinit specifier ensures that the variable is initialized during static initialization ([basic.start.static]).
end note
]

The const in constinit refers only to the initialization, not the variable, not any types. Note that it also doesn't change the kind of initialization performed, it merely diagnoses if the wrong kind is performed.

In:

constinit int a = 0;
constexpr int b = a;

0 is a constant expression, so the initialization of a is well-formed. Once we get past that, the specifier doesn't do anything. It's equivalent to:

int a = 0; // same behavior, a undergoes constant initialization
constexpr int b = a;

Which is straightforwardly ill-formed.



but at constant-initialization, its value is known, so it could be used to initialize b.

Sure, at this moment. What about:

constinit int a = 0;
cin >> a;
constexpr int b = a;

That's obviously not going to fly. Allowing this would require extending what a constant expression is (already the most complex rule in the standard, in my opinion) to allow for non-constant variables but only immediately after initialization? The complexity doesn't seem worth it, since you can always write:

constexpr int initializer = 0;
constinit int a = initializer;
constexpr int b = initializer;


Related Topics



Leave a reply



Submit