Constant Expressions and Compile-Time Optimization

The as-if rule

The as-if rule is a fundamental principle in the C++ programming language that guides compiler optimization. This rule is defined in the C++ standard and essentially states that compilers are permitted to perform any optimization as long as the observable behavior of the program remains the same as if the optimization were not applied.

In other words, if the optimized program produces the same results as the non-optimized version according to the observable behavior defined by the C++ standard, then the optimization is valid.

Exactly how a compiler optimizes a given program is up to the compiler itself. However, there are things we can do to help the compiler optimize better.

An Optimization Opportunity

Consider the following short program:

#include <iostream>

int main()
{
	int x { 3 + 4 };
	std::cout << x << '\n';

	return 0;
}

The output is straightforward:

7

However, there's an interesting optimization possibility hidden within.

If this program were compiled exactly as it was written (with no optimizations), the compiler would generate an executable that calculates the result of 3 + 4 at runtime (when the program is run). If the program is executed a million times, 3 + 4 would be evaluated a million times, and the resulting value of 7 produced a million times.

Because the result of 3 + 4 never changes (it is always 7), re-calculating this result every time the program is run is wasteful.

Compile-Time Evaluation of Expressions

Modern C++ compilers are able to evaluate some expressions at compile-time. When this occurs, the compiler can replace the expression with the result of the expression.

For example, the compiler could optimize the above example to this:

#include <iostream>

int main()
{
	int x { 7 };
	std::cout << x << '\n';

	return 0;
}

This program produces the same output (7) as the prior version, but the resulting executable no longer needs to spend CPU cycles calculating 3 + 4 at runtime.

Such optimizations make our compilation take longer (because the compiler has to do more work), but because expressions only need to be evaluated once at compile-time (rather than every time the program is run) the resulting executables are faster and use less memory.

The ability for C++ to perform compile-time evaluation is one of the most important and evolving areas of modern C++.

Constant Expressions

One kind of expression that can always be evaluated at compile time is called a “constant expression”. The precise definition of a constant expression is complicated.

A compile-time constant is a constant whose value must be known at compile time.
For example:

  • Literals = ‘5’, ‘1.7’
  • Const integral variables with a constant expression initializer (e.g. const int x { 1 };)
  • Constexpr variable that we discussing in this chapter.

Const variables that are not compile-time constants are called runtime constants. Runtime constants cannot be used in a constant expression.

The most common type of operators and functions that support compile-time evaluation include:

  • Arithmetic operators with operands of fundamental types (e.g. 1 + 2)
  • Constexpr and consteval functions that we will discuss in this chapter.

We identify the constant expressions and non-constant expressions. We also identify which variables are non-constant, runtime constant, or compile-time constant.

#include <iostream>

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

    return y;
}

int main()
{
    // Non-const variables:
    int a { 5 };                 // 5 is a constant expression
    double b { 1.2 + 3.4 };      // 1.2 + 3.4 is a constant expression

    // Const integral variables with a constant expression initializer
    // are compile-time constants:
    const int c { 5 };           // 5 is a constant expression
    const int d { c };           // c is a constant expression
    const long e { c + 2 };      // c + 2 is a constant expression

    // Other const variables are runtime constants:
    const int f { a };           // a is not a constant expression
    const int g { a + 1 };       // a + 1 is not a constant expression
    const long h { a + c };      // a + c is not a constant expression
    const int i { getNumber() }; // getNumber() is not a constant expression

    const double j { b };        // b is not a constant expression
    const double k { 1.2 };      // 1.2 is a constant expression

    return 0;
}

An expression that is not a constant expression is sometimes called a runtime expression. For example, std::cout << x <<'\n' is a runtime expression, both because x is not a compile-time constant, because operator<< doesn't support compile-time evaluation when used for output (since output can't be done at compile-time).