Guaranteeing compile-time evaluation

In the previous lesson, we learned that constant expressions can be evaluated at compile-time, but the compiler only must do so in contexts requiring constant expressions. We also saw that const integral variables with constant initializers qualify as constant expressions - but const floating-point variables don't.

This creates problems:

const double pi{ 3.14159 };           // NOT usable in constant expressions
const int radius{ 10 };               // usable (const integral with constant initializer)
const double area{ pi * radius * radius }; // NOT usable (pi isn't)

We want pi and area to be compile-time constants, but const doesn't provide that for non-integral types. We need something stronger.

Enter constexpr

The constexpr keyword (short for "constant expression") declares a variable as a compile-time constant. Unlike const, which merely prevents modification, constexpr requires that the initializer be evaluatable at compile-time:

constexpr double pi{ 3.14159 };        // compile-time constant
constexpr int radius{ 10 };            // compile-time constant
constexpr double area{ pi * radius * radius }; // works! pi is constexpr

All three variables are now usable in constant expressions. The compiler enforces this - if you try to initialize a constexpr variable with a runtime value, compilation fails:

#include <iostream>

int main()
{
    std::cout << "Enter a number: ";
    int input{};
    std::cin >> input;

    constexpr int value{ input };  // ERROR: input is not a constant expression

    return 0;
}

This compile-time error is exactly what we want. If we need a compile-time constant and accidentally provide a runtime value, we learn immediately rather than silently getting a runtime variable.

What constexpr does differently

Consider this comparison:

const int runtimeValue{ getUserInput() };  // OK: const allows runtime initialization
constexpr int compileValue{ getUserInput() }; // ERROR: constexpr requires compile-time value

With const, the compiler happily creates a runtime constant - a variable you can't modify but whose value is computed when the program runs. With constexpr, the compiler refuses to proceed unless it can determine the value during compilation.

This strictness is the point. When you use constexpr, you're telling the compiler: "I need this value at compile-time. If that's not possible, tell me now."

Function return values

Normal functions execute at runtime, so their return values can't initialize constexpr variables:

int computeMax()
{
    return 100;
}

int main()
{
    constexpr int max{ computeMax() };  // ERROR: computeMax() is not constexpr
    return 0;
}

Even though computeMax() clearly returns 100, the compiler doesn't analyze function bodies this way. The return value of a non-constexpr function is a runtime expression, period.

Later, we'll learn about constexpr functions that can be evaluated at compile-time. For now, just know that regular function calls can't initialize constexpr variables.

Choosing between const and constexpr

Both keywords create constants, but they serve different purposes:

const means "this value won't change after initialization":

  • The initializer can be a compile-time or runtime value
  • Useful when you need a constant but the value comes from runtime sources
  • Only const integral types with constant initializers work in constant expressions

constexpr means "this is a compile-time constant":

  • The initializer must be a constant expression
  • The variable is usable in all contexts requiring constant expressions
  • Works with any type that supports compile-time evaluation
int getConfig();  // returns some runtime configuration

const int setting{ getConfig() };     // runtime constant - can't change, but not compile-time
constexpr int defaultSetting{ 100 };  // compile-time constant

const double gravity{ 9.81 };         // NOT usable in constant expressions (const double)
constexpr double gravity{ 9.81 };     // usable in constant expressions
Best Practice
Use `constexpr` for variables whose values are known at compile-time. Use `const` for variables that shouldn't change but whose values come from runtime sources.

Constexpr implies const

Every constexpr variable is automatically const - you can't modify it after initialization. Writing constexpr const is redundant:

constexpr int value{ 42 };       // implicitly const
constexpr const int same{ 42 };  // explicit const is unnecessary

However, constexpr is not part of the type. A variable declared constexpr int has type const int. The constexpr keyword is a declaration specifier that controls how the variable can be initialized, not what type it has.

Function parameters can't be constexpr

Function parameters get their values at runtime (from arguments passed by callers), so they can't be constexpr:

void process(constexpr int value)  // ERROR: parameters can't be constexpr
{
    // ...
}

Even const parameters are runtime constants - the constness just means the function won't modify the copy it receives:

void display(const int value)  // OK: value won't be modified
{
    // But value is still a runtime constant, not compile-time
    constexpr int copy{ value };  // ERROR: value is not a constant expression
}

Type compatibility

Most fundamental types work with constexpr: integers, floating-point numbers, characters, booleans, and pointers to constant data.

Some standard library types don't fully support constexpr:

#include <string>
#include <string_view>

constexpr std::string text{ "hello" };       // Problematic (uses dynamic allocation)
constexpr std::string_view view{ "hello" };  // OK (no dynamic allocation)

For strings, use std::string_view when you need a constexpr string constant. We'll cover this in detail when we discuss string types.

Preview: constexpr functions

We mentioned that regular functions can't provide values for constexpr variables. C++ does support constexpr functions - functions declared with the constexpr keyword that can be evaluated at compile-time when given constant arguments:

constexpr int square(int n)
{
    return n * n;
}

int main()
{
    constexpr int result{ square(5) };  // OK: evaluated at compile-time
    return 0;
}

We'll cover constexpr functions in depth in a later lesson. For now, just understand that they exist and can be called in constant expressions.

Summary

  • constexpr declares a variable as a compile-time constant
  • Initialization requirement: constexpr variables must be initialized with constant expressions - the compiler rejects runtime values
  • Error catching: Unlike const, constexpr fails compilation if you accidentally try to use a runtime value
  • All types supported: constexpr works with floating-point, pointers, and other types that const doesn't make compile-time
  • Implicitly const: constexpr variables are automatically const (can't be modified)
  • Not a type: constexpr is a declaration specifier, not part of the type - constexpr int has type const int
  • No constexpr parameters: Function parameters are initialized at runtime, so they can't be constexpr
  • Choose wisely: Use constexpr for compile-time constants, const for runtime constants
  • constexpr functions: Functions can be constexpr too, allowing compile-time computation (covered later)

When you need a value to be available at compile-time, use constexpr. The compiler will enforce your intent and catch mistakes early.