Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Constant Expressions
Write expressions that the compiler can evaluate at compile time for better performance.
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).
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.
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.
Constant Expressions - Quiz
Test your understanding of the lesson.
Practice Exercises
Constant Expression Calculations
Create constant expressions that the compiler can evaluate at compile time for better performance.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!