As of C++20, we have four keywords beginning with const. What do they all mean? Are they mostly the same? Let’s compare them in this article.

const vs constexpr  

const, our good old fried from the early days of C++ (and also C), can be applied to objects to indicate immutability. This keyword can also be added to non-static member functions, so those functions can be called on const instances of a given type.

const doesn’t imply any “compile-time” evaluation. The compiler can optimize the code and do so, but in general, const objects are initialized at runtime:

// might be optimized to compile-time if compiled decides...
const int importantNum = 42;

// will be inited at runtime
std::map<std::string, double> buildMap() { /*...*/ }
const std::map<std::string, double> countryToPopulation = buildMap();

const can sometimes be used in “constant expressions”, for example:

const int count = 3;
std::array<double, count> doubles {1.1, 2.2, 3.3};

// but not double:
const double dCount = 3.3;
std::array<double, static_cast<int>(dCount)> moreDoubles {1.1, 2.2, 3.3};
// error: the value of 'dCount' is not usable in a constant expression

See at Compiler Explorer

Let’s see the full definition from cppreference:

Defines an expression that can be evaluated at compile time. Such expressions can be used as non-type template arguments, array sizes, and in other contexts that require constant expressions.

If you have a constant integral variable that is const initialized, or enumeration value, then it can be used at constant expression.

Since C++11, we have a new keyword - constexpr - which pushed further the control over variables and functions that can be used in constant expressions. Now it’s not a C++ trick or a special case, but a complete, easier-to-understand solution.

// fine now:
constexpr double dCount = 3.3;
std::array<double, static_cast<int>(dCount)> doubles2 {1.1, 2.2, 3.3};

Above, the code uses double, which is allowed in constant expressions.

We can also create and use simple structures:

#include <array>
#include <cstdlib>

struct Point {
    int x { 0 };
    int y { 0 };

    constexpr int distX(const Point& other) const { return abs(x - other.x); }
};

int main() {
    constexpr Point a { 0, 1 };
    constexpr Point b { 10, 11 };
    static_assert(a.distX(b) == 10);
    
    // but also at runtime:
    Point c { 100, 1 };
    Point d { 10, 11 };
    return c.distX(d);
}

Run @Compiler Explorer

In summary:

  • const can be applied to all kinds of objects to indicate their immutability
  • const integral type with constant initialization can be used in constant expression
  • constexpr object, by definition, can be used in constant expressions
  • constexpr can be applied to functions to show that they can be called to produce constant expressions (they can also be called at runtime)
  • const can be applied to member functions to indicate that a function doesn’t change data members (unless mutable),

constexpr vs consteval  

Fast forward to C++20, we have another keyword: consteval. This time it can be applied only to functions and forces all calls to happen at compile time.

For example:

consteval int sum(int a, int b) {
  return a + b;
}

constexpr int sum_c(int a, int b) {
    return a + b;
}

int main() {
    constexpr auto c = sum(100, 100);
    static_assert(c == 200);

    constexpr auto val = 10;
    static_assert(sum(val, val) == 2*val);

    int a = 10;
    int b = sum_c(a, 10); // fine with constexpr function

    // int d = sum(a, 10); // error! the value of 'a' is 
                           // not usable in a constant expression
}

See @Compiler Explorer.

Immediate functions can be seen as an alternative to function-style macros. They might not be visible in the debugger (inlined)

Additionally, while we can declare a constexpr variable, there’s no option to declare a consteval variable.

// consteval int some_important_constant = 42; // error

In summary:

  • consteval can only be applied to functions
  • constexpr can be applied to functions and also variables
  • consteval forces compile time function evaluation; the constexpr function can be executed at compile time but also at runtime (as a regular function).

This article started as a preview for Patrons, sometimes even months before the publication. If you want to get extra content, previews, free ebooks and access to our Discord server, join the C++ Stories Premium membership or see more information.

constexpr vs constinit  

And now the last keyword: constinit.

constinit forces constant initialization of static or thread-local variables. It can help to limit static order initialization fiasco by using precompiled values and well-defined order rather than dynamic initialization and linking order…

#include <array>

// init at compile time
constexpr int compute(int v) { return v*v*v; }
constinit int global = compute(10);

// won't work:
// constinit int another = global;

int main() {
    // but allow to change later...
    global = 100;

    // global is not constant!
    // std::array<int, global> arr;
}

See at Compiler Explorer

Contrary to const or constexpr, it doesn’t mean that the object is immutable. What’s more constinit variable cannot be used in constant expressions! That’s why you cannot init another with global or use global as an array size.

In summary:

  • constexpr variables are constant and usable in constant expressions
  • constinit variables are not constant and cannot be used in constant expressions
  • constexpr can be applied on local automatic variables; this is not possible with constinit, which can only work on static or thread_local objects
  • you can have a constexpr function, but it’s not possible to have a constinit function declaration

Mixing  

Can we have mixes of those keywords?

// possible and compiles... but why not use constexpr?
constinit const int i = 0;

// doesn't compile:
constexpr constinit int i = 0;

// compiles:
const constexpr int i = 0;

And we can have const and constexpr member functions:

struct Point {
    int x { 0 };
    int y { 0 };

    constexpr int distX(const Point& other) const { return abs(x - other.x); }
    constexpr void mul(int v) { x *= v; y *= v; }
};

In the above scenario, constexpr means that the function can be evaluated for constant expressions, but const implies that the function won’t change its data members. There’s no issue in having only a constexpr function like mul:

constexpr int test(int start) {
    Point p { start, start };
    p.mul(10); // changes p!
    return p.x;
}

int main() {
    static_assert(test(1) == 10);
}

Run at Compiler Explorer

Summary  

After seeing some examples, we can build the following summary table:

keyword on auto variables to static/thread_local variables to functions constant expressions
const yes yes as const member functions sometimes
constexpr yes or implicit (in constexpr functions) yes to indicate constexpr functions yes
consteval no no to indicate consteval functions yes (as a result of a function call)
constinit no to force constant initialization no no, a constinit variable is not a constexpr variable

You can also find another cool article from Marius Bancila where he also compared those keywords (back in 2019): Let there be constants!.

As of C++23 there’s no new const keyword added, so the list I showed in this article should be valid for a couple of years at least :)

Back to you

  • Do you use constexpr?
  • Have you tried constinit?

Share your feedback in comments below.