When does evaluation happen?

Every expression in your program eventually produces a value. The question is: when?

Some expressions must wait until the program runs:

int userChoice{};
std::cin >> userChoice;  // can't know this at compile-time

The user hasn't typed anything yet when you compile the program, so this expression can only evaluate at runtime.

Other expressions could theoretically evaluate earlier:

int area{ 5 * 10 };  // both values are known at compile-time

Both operands are literals. The compiler knows them. Should the multiplication happen now (during compilation) or later (when the program runs)?

The answer depends on several factors we'll explore in this lesson.

Compile-time vs runtime evaluation

In the previous lesson, we saw how compilers can optimize programs by evaluating expressions at compile-time instead of runtime. But the as-if rule leaves this decision entirely to the compiler. You might want a calculation done at compile-time, but you have no guarantee it will be.

C++ provides a more explicit approach: constant expressions. These are expressions that the language guarantees can be evaluated at compile-time. When you use constant expressions in contexts that require compile-time values, the compiler must evaluate them during compilation.

What makes an expression "constant"?

A constant expression is an expression where every component can be determined at compile-time. This includes:

Literals - Values written directly in code:

42              // integer literal
3.14159         // floating-point literal
"game over"     // string literal
true            // boolean literal

Const integral variables with constant initializers:

const int maxScore{ 1000 };           // constant expression
const int bonusThreshold{ maxScore / 2 };  // also constant (uses maxScore)

Operators with constant operands:

100 + 50        // constant: both operands are literals
maxScore * 2    // constant: maxScore is const int, 2 is literal
sizeof(double)  // constant: type sizes are known at compile-time

Constexpr variables and functions (covered in the next lesson).

What prevents an expression from being constant?

An expression becomes a runtime expression (non-constant) if any part of it cannot be determined at compile-time:

int playerHealth{ 100 };                   // non-const variable
const int savedHealth{ playerHealth };      // runtime: initializer is non-const

const double dropRate{ 0.15 };             // const non-integral type
const double adjustedRate{ dropRate * 2 }; // runtime: dropRate can't be used

int getInput();                            // non-constexpr function
const int choice{ getInput() };            // runtime: function result unknown

Notice the second example: const double with a constant initializer is not usable in constant expressions. This surprises many programmers. Only const integral types (int, char, long, etc.) get this special treatment. For floating-point and other types, you need constexpr (next lesson).

Important
These cannot appear in constant expressions: - Non-const variables - Const non-integral variables (even with constant initializers) - Return values from non-constexpr functions - Function parameters

Historical note: Const integral types were grandfathered into constant expressions because older C++ code already treated them as compile-time constants. The standards committee decided not to extend this to const floating-point types to encourage consistent use of constexpr.

Why this matters: required vs optional

Some contexts require constant expressions:

constexpr int arraySize{ 100 };  // constexpr requires constant initializer
int data[arraySize];              // array length must be constant expression

template<int N>                   // template arguments must be constant
struct Buffer { /* ... */ };

If you provide a runtime expression where a constant expression is required, compilation fails.

Other contexts accept constant expressions but don't require them:

const int threshold{ 50 + 25 };  // constant expression, evaluated at compile-time
int current{ 50 + 25 };          // constant expression, but may evaluate at runtime

Both initializers are 50 + 25, a constant expression. The difference is that threshold (being a const integral) can itself be used in constant expressions, so its initializer must be evaluated at compile-time. For current, the compiler may choose to evaluate at compile-time (likely with optimizations enabled) or runtime.

Key Concept
Constant expressions are only *required* to be evaluated at compile-time in contexts that demand constant expressions. In other contexts, the compiler decides.

Categorizing evaluation likelihood

How likely is an expression to be evaluated at compile-time?

Category Description Example
Never Runtime expression with values unknown at compile-time getUserInput() * 2
Possibly Runtime expression but compiler might optimize nonConstVar + 10 (where value is trackable)
Likely Constant expression in non-requiring context int x{ 5 + 3 };
Always Constant expression in requiring context constexpr int x{ 5 + 3 };

Benefits of compile-time evaluation

Why care whether evaluation happens at compile-time or runtime?

Performance: Work done at compile-time doesn't happen at runtime. Your program starts faster and runs leaner.

Error detection: Many bugs become compile-time errors instead of runtime crashes. Divide by zero in a constant expression? Compilation fails. Integer overflow? The compiler catches it.

No undefined behavior: Undefined behavior isn't permitted at compile-time. If your constant expression would cause UB, the compiler rejects it rather than producing a program that might crash, corrupt data, or behave unpredictably.

Guaranteed optimization: Code that must be constant will be constant. You don't have to hope the optimizer is smart enough.

Terminology

When reading C++ documentation, you'll encounter these terms:

  • Constant expression: An expression that can be evaluated at compile-time
  • Runtime expression: An expression that must wait until runtime
  • Manifestly constant-evaluated: Technical term for expressions that must be evaluated at compile-time (in contexts requiring constant expressions)

You'll also see two ways of phrasing:

  • "X is usable in a constant expression" - emphasizes what X is
  • "X is a constant expression" - emphasizes the whole expression

Both mean the same thing in practice.

Summary

  • Constant expressions contain only elements determinable at compile-time: literals, const integral variables with constant initializers, constexpr entities, and operators with constant operands
  • Runtime expressions contain at least one element unknown at compile-time
  • Const integral variables with constant initializers are usable in constant expressions (historical exception)
  • Const non-integral variables are NOT usable in constant expressions, even with constant initializers - use constexpr instead
  • Required contexts (like constexpr initializers, array sizes, template arguments) must receive constant expressions
  • Optional contexts allow the compiler to choose whether to evaluate at compile-time
  • Benefits include better performance, earlier error detection, elimination of undefined behavior, and guaranteed optimization
  • The compiler only guarantees compile-time evaluation when a constant expression is required; otherwise, it's an optimization choice

Understanding constant expressions is essential for effective C++ programming. They form the foundation for constexpr variables and functions, which we'll explore next.