Skip to Content
Course content

93: constexpr Functions and Compile-Time Computation

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I was working on a project recently where I needed a static lookup table for some physics calculations. I knew the size of the table had to be based on a specific mathematical formula—essentially a factorial of a small constant—and since it was a fixed-size array, I wanted it to be decided at compile-time. No heap allocations, no runtime overhead.

The Array Size Headache

My first instinct was to just write a quick helper function. I figured the compiler would be smart enough to see it was a simple calculation and just "plug it in." Here is what I tried:

int get_table_size(int n) {
    if (n <= 1) return 1;
    return n * get_table_size(n - 1);
}

int main() {
    // I want a buffer based on 5! (120)
    int buffer[get_table_size(5)]; 
    return 0;
}

I hit compile and, predictably, the compiler yelled at me. It told me that the array bound must be a constant expression. Even though get_table_size(5) is obviously 120 to any human, the compiler sees a function call. To the compiler, a function call is a "runtime event"—something that happens while the program is actually running, not while it's being built. It can't guarantee that get_table_size won't do something unpredictable, so it refuses to use it to size a stack array.

Teaching the Compiler to Do the Math

This is where constexpr comes in. By adding this keyword, I'm essentially telling the compiler: "If the inputs to this function are known at compile-time, you are allowed—and encouraged—to evaluate this entire function right now, during the build process."

Let's tweak the code:

constexpr int get_table_size(int n) {
    if (n <= 1) return 1;
    return n * get_table_size(n - 1);
}

int main() {
    // Now this works perfectly.
    int buffer[get_table_size(5)]; 
    return 0;
}

Now it compiles. The compiler evaluates get_table_size(5), replaces it with the literal 120, and the binary that actually ships to the user contains no call to that function for the array sizing. It's essentially a zero-cost abstraction.

The Dual-Purpose Nature of constexpr

One thing that always tripped me up when I first learned this is whether a constexpr function must be evaluated at compile-time. I wondered: "If I mark it constexpr, can I still use it with a variable that the user types in at runtime?"

I decided to test it out:

constexpr int get_table_size(int n) {
    if (n <= 1) return 1;
    return n * get_table_size(n - 1);
}

int main() {
    int user_val;
    std::cin >> user_val;
    
    // Can I use a constexpr function here?
    int result = get_table_size(user_val); 
    std::cout << "Result: " << result;
}

It works. This is the "magic" of constexpr. It doesn't force compile-time evaluation; it enables it. If the arguments are constants, it happens at compile-time. If the arguments are runtime variables, it behaves like a totally normal function. You get one piece of code that handles both scenarios optimally.

Hitting the Wall

Of course, you can't put just anything in a constexpr function. I tried to add a print statement inside my factorial function just to see the recursion happening during compilation:

constexpr int get_table_size(int n) {
    std::cout << "Calculating for " << n << "\n"; // Oops
    if (n <= 1) return 1;
    return n * get_table_size(n - 1);
}

The compiler immediately complained. Why? Because std::cout is a runtime object. You cannot perform I/O operations during the compilation process. The compiler is essentially a program that reads your code and writes a binary; it isn't "running" your program in a virtual machine. It can only perform calculations and logic that can be resolved purely through the language's type system and constant values.

Basically, if the operation requires the operating system (like printing to a console, reading a file, or allocating memory via malloc), it cannot be constexpr.




📋 Practical Task

Exercise: Pre-calculating a Coordinate Grid Capacity

You are building a 3D game engine. To optimize memory, you need to pre-calculate the total number of cells in a nested coordinate grid based on three dimensions (X, Y, and Z), but these dimensions are constants defined by the game's level settings.

Your Task:

  • Create a constexpr function named calculate_grid_cells that takes three integers (width, height, depth) and returns their product.
  • In your main function, define three constexpr int variables for lvl_width, lvl_height, and lvl_depth.
  • Use the calculate_grid_cells function to initialize a std::array of integers. The size of the array must be determined by your constexpr function.
  • Print the size of the array to the console to verify the calculation was performed.

Goal: Ensure the array is sized at compile-time, meaning the function call must be valid as a template argument for std::array.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.