Let there be constants!

In the beginning, there was const. And people saw that const was good. And then the people said: let there be constexpr, and consteval, and constinit. And thus, starts this article about constant functions and variables in C++20, which I will try to keep short and concise.

const

const is a specifier that indicates that a global, local, or member variable or a function parameter cannot be modified after it is initialized. It can also be used to qualify a non-static member function; such a constant member function cannot modify the object state (unless the fields are declared mutable) and can only invoke other constant member functions. Such a function, however, is still evaluated at runtime.

class foo
{
   int value;
public:
   foo(int const v) : value(v) {}

   int get() const { return value; }
};

int main()
{
   int const fortyTwo = 42;
   foo f(fortyTwo);
   int n = f.get();
}

The term const correctness refers to using constants whereever is possible. However, the exact placement of the const specifier has generated a great debate within the C++ community between those that advocate the use of it on the left or right side of the type. These alternatives are called East const and West const.

int const a = 42;  // East const
const int a = 42;  // West const

I have been using East const for many years and I believe it’s the better alternative. I will not get into the details here, but you can read more about it here: Join the East const revolution!

As of C++17, constants (and variables in general) can be declared inline. This makes it possible to define global constants in a header file, or initializing static const members of a class in a header, without risking to generate multiple definitions for the same symbol when the header is included in more than one translation unit.

// foo.h header

struct foo
{
   static const int alpha;
};

inline const int foo::alpha = 42;

inline const int beta = 44;

struct bar
{
   inline static const int gamma = 42;
};

constexpr

The constexpr specifier was introduced in C++11 to indicate that a variable or function can appear in a constant expression, which is an expression that can be evaluated at compile time.

constexpr int maximum(int const a, int const b)
{
   return a >= b ? a : b;
}

int main()
{
   int const Size = 64;
   int arr[maximum(42, Size)]{ 0 };
}

Specifying constexpr for a function does not mean that the function is always evaluated at compile-time. This is done only when it’s possible. If invoked with arguments that are not constant expressions, the evaluation will only happen at runtime, as shown in the following example.

constexpr int m1 = maximum(42, 66);  // compile-time evaluation
int a = 42, b = 66;
const int m2 = maximum(a, b);        // run-time evaluation

A constexpr specifier used in an object declaration implies const. A constexpr specifier used in a function or static member variable declaration implies inline. If any declaration of a function or function template has a constexpr specifier, then every declaration must contain the specifier.

constexpr can be used not only with variables and functions, but also member functions, constructors, and as of C++20 with virtual functions. There are various other changes in C++20 related to constexpr:

  • can use try-catch blocks in a constexpr function as long as no exception is thrown from the function;
  • it is possible to change the active member of a union inside constexpr;
  • it is possible to use dynamic_cast and polymorphic typeid in constant expressions;
  • std::string, std::vector, and other library types are constexpr;
  • std::is_constant_evaluated() added to allow to check if code is actually executed within a constant evaluation.

consteval

The consteval specifier is a new feature in C++20 that is used to specify that a function is an immediate function, which means that the function must always produce a constant expression. This has the implication that the function is only seen at compile time. Symbols are not emitted for the function, you cannot take the address of such a function, and tools such as debuggers will not be able to show them. In this matter, immediate functions are similar to macros.

consteval int maximum(int const a, int const b)
{
   return a >= b ? a : b;
} 

constexpr int m1 = maximum(42, Size);   // OK, compile-time evaluation
int a = 12, b = 66;
const int m2 = maximum(a, b);           // error

using fptr = int(int, int);
fptr* pmax = maximum;                   // error

A consteval specifier implies inline. If any declaration of a function or function template contains a consteval specifier, then all declarations of that function or function template must contain the specifier. A function that is consteval is a constexpr function, and must satisfy the requirements applicable to constexpr functions (or constexpr constructors).

constinit

Before getting to the constinit specifier, let’s talk about initialization. There are various forms of initialization in C++. Zero initialization sets the initial value of an object to zero. It happens in several situations:

// 1
static T object;

// 2
T ();
T t = {};
T {};

// 3
CharT array [ n ] = "";

Zero initialization is performed for every named variable with static or thread-local duration when constant-initialization does not happen and occurs before any other initialization.

Constant initialization sets the value of a static variable to a compile-time expression. It can have the following forms:

static T & ref = constexpr;
static T object = constexpr;

Constant initialization is performed instead of zero initialization. Together, zero-initialization and constant initialization are called static initialization and all other initialization is called dynamic initialization. All static initialization happens before any dynamic initialization.

Variables with static storage duration that have dynamic initiliazation could cause bugs that are hard to find. Consider two static objects, A and B, initialized in different translation units. If the initialization of one of the objects, let’s say B, depends on the other object (for intance by invoking a member of that object), then the initialization may succeed, if the other object is already initialized, or fail, if the object is not initialized already. This result depends on the order of the initialization of the translation units, which is not deterministic.

On the other hand, variables with static duration that have constant initialization are initialized at compile time and therefore can be safely used when performing dynamic initialization of translation units.

The constinit specifier can be applied to variables with static storage duration and requires the variable to have a constant initializer. This specifier helps communicating the intention to both the compiler and other programmers. The specifier can be used on any declaration of a variable; however, if it is present on some declaration but not on the initializing declaration, the program is ill-formed.

The following is an example from the P1143R2 paper.

char const * g() { return "dynamic initialization"; }
constexpr char const * f(bool p) { return p ? "constant initializer" : g(); }

constinit char const* c = f(true);   // OK.
constinit char const* d = f(false);  // ill-formed

Keep in mind that…

At most one of the constexpr, consteval, and constinit specifiers is allowed to appear within the same sequence of declaration specifiers.

See also

For more information about these topics see:

7 Replies to “Let there be constants!”

  1. Regarding consteval functions:

    > you cannot take the address of such a function

    That’s not correct. You cannot “leak the address of such a function” to non-constant-expression contexts (which, in practice, is meant to prevent the need to emit the function in object code), but it’s perfectly all right (and useful!) to take the address within constant-expressions. E.g., the (consteval) members_of API in P1240 counts on that to be able to pass the address of consteval predicates (like is_public) as trailing arguments.

  2. inline int foo::alpha = 42; // there should be ‘const’ otherwise it makes: error: conflicting declaration
    But primarily ‘inline’ is convenient as one can use it for a a single place initialization inside the class definition:
    struct foo {
    static inline const int alpha = 42;
    };

  3. Thanks for the corrections. I have updated the snippet. (I’m pretty sure I tested the code with the VC++ compiler at that time and worked. But with VC++ 2019 16.5 it does raise the error you mentioned.)

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.