Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 7 of 8

Two keywords that look similar and answer different questions. const asks can this change? constexpr asks when is this known?

const: this will not change

#include <iostream>

int main() {
    const int max_users = 100;
    int current = 42;

    std::cout << max_users << " " << current << "\n";
    current = 43;
    std::cout << current << "\n";
    return 0;
}
100 42
43

Try to modify a const and the compiler stops you:

// WILL NOT COMPILE
int main() {
    const int max_users = 100;
    max_users = 200;
    return 0;
}

error: assignment of read-only variable 'max_users'

A const must be initialized where it's declared — there's no later opportunity.

Why bother?

Three real payoffs, in increasing order of importance:

It documents intent. const int max_retries = 3; says this is a fixed policy, not a variable someone will tune at run time. The next reader knows without checking.

It catches mistakes. The classic is = where you meant ==:

// WILL NOT COMPILE
int main() {
    const int limit = 10;
    int value = 5;
    if (limit = value) {   // assignment, not comparison — caught because limit is const
        return 1;
    }
    return 0;
}

Without const that compiles fine, silently assigns, and behaves nothing like you intended.

It enables optimization. A compiler that knows a value can't change can make assumptions it otherwise couldn't.

Named constants beat magic numbers

#include <iostream>

int main() {
    const double tax_rate = 0.10;
    const int free_shipping_threshold = 50;

    double subtotal = 60.0;
    double total = subtotal * (1.0 + tax_rate);
    bool free_shipping = subtotal >= free_shipping_threshold;

    std::cout << total << "\n";
    std::cout << std::boolalpha << free_shipping << "\n";
    return 0;
}
66
true

Compare that to subtotal * 1.10 scattered through a codebase. When the tax rate changes you edit one line instead of hunting for every 1.10 — and you avoid the ones that were coincidentally 1.10 for unrelated reasons.

constexpr: known at compile time

constexpr is a stronger promise: not just "won't change," but "the value is computable while compiling, before the program runs."

#include <iostream>

int main() {
    constexpr int buffer_size = 1024;
    constexpr int half = buffer_size / 2;

    int array[buffer_size];
    array[0] = 7;

    std::cout << buffer_size << " " << half << " " << array[0] << "\n";
    std::cout << sizeof(array) / sizeof(array[0]) << "\n";
    return 0;
}
1024 512 7
1024

half is computed by the compiler; the running program contains 512 as a literal, with no division performed at run time.

The array size is where the difference becomes visible. C-style array sizes must be known at compile time — a constexpr works there and an ordinary variable doesn't:

int size = 10;        // a run-time value
int array[size];      // not standard C++

Standard C++ forbids this. GCC and Clang nevertheless accept it as a compiler extension (inherited from C, where it's legal), so it may well compile silently on your machine — add -Wpedantic and you'll get warning: ISO C++ forbids variable length array.

Don't rely on it. Use std::vector (lesson 6.3) when the size isn't known until run time; that's the portable answer and it's what you'd want anyway.

constexpr functions

Functions can be constexpr too, meaning "callable at compile time when the arguments allow":

#include <iostream>

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

int main() {
    constexpr int compile_time = square(12);
    int runtime_input = 7;
    int run_time = square(runtime_input);

    std::cout << compile_time << " " << run_time << "\n";

    int grid[square(4)];
    grid[0] = 1;
    std::cout << sizeof(grid) / sizeof(grid[0]) << "\n";
    return 0;
}
144 49
16

The same function serves both: called with a constant it's evaluated during compilation; called with a run-time value it runs normally. square(4) as an array size works precisely because the compiler can evaluate it.

Module 22 goes much deeper into compile-time computation. For now, the useful habit is simply to mark genuinely fixed values constexpr rather than const.

Choosing between them

| | const | constexpr | | --- | --- | --- | | Means | won't change after initialization | value known at compile time | | Initializer | may be computed at run time | must be a compile-time constant | | Array sizes | no | yes | | Use when | a value shouldn't be modified | a value is genuinely fixed in the source |

constexpr implies const — a constexpr value can't change either. So:

constexpr int max_retries = 3;                 // fixed in source → constexpr
const int user_limit = readConfigValue();      // fixed once known → const

The rule of thumb: constexpr if you can, const if you can't, neither only if the value genuinely varies.

Try it

Write a program with a constexpr function cube(int), use it to size a C-style array, and also call it with a value read from a normal int variable. Print both results.

Checkpoint

  • const means the value cannot be modified after initialization; it must be initialized at declaration.
  • const documents intent, catches =-for-== mistakes, and helps the optimizer.
  • Named constants beat magic numbers scattered through code.
  • constexpr means the value is computed at compile time — required for C-style array sizes.
  • constexpr functions can run at compile time or run time depending on their arguments.
  • Prefer constexpr where possible, const otherwise.

Questions about this lesson

Log in to ask a question.