const & constexpr Basics

Reviewed & published by Brayan K

Some values should never change: a tax rate, the number of squares on a chessboard, pi. Marking them const tells the compiler to reject any accidental reassignment, and constexpr goes further by computing values at compile time. "Const-correctness" — using const everywhere you can — is one of the habits that separates solid C++ from buggy C++. This lesson makes it second nature.

Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

Think of const as writing a value in permanent ink instead of pencil — you can read it forever, but you can't erase and rewrite it. constexpr is stronger still: it's like a value engraved into the building's cornerstone before the doors even open — fixed at construction time (compile time), so it can be used as part of the structure itself, such as deciding how many rooms (array slots) to build. Both protect you: if you (or a teammate) ever try to change something that was meant to be fixed, the compiler stops you immediately instead of letting a subtle bug slip through.

📊 const vs constexpr

Aspectconstconstexpr
Can be reassigned?nono
Known when?run time (or compile)compile time (guaranteed)
Can size an array?only if also compile-timeyes
Initializerany expressionmust be a constant expression
Typical useread-only values, paramstrue compile-time constants

Rule of thumb: use const by default for anything you don't intend to change; promote to constexpr when the value is a fixed compile-time constant.

1. const: Read-Only Values

Add const before a type and the variable becomes read-only: you must initialize it on the spot, and any later attempt to reassign it is a compile error. This is a feature, not a restriction — it documents intent ("this never changes") and lets the compiler catch accidental writes. By convention, constants are named in UPPER_SNAKE_CASE.

#include <iostream>
using namespace std;

int main() {
    // const = read-only once set. The compiler blocks any reassignment.
    const double PI = 3.14159;
    const int    MAX_USERS = 100;

    cout << "PI = " << PI << endl;            // PI = 3.14159
    cout << "max users = " << MAX_USERS << endl; // max users = 100

    // PI = 3.14;          // ❌ error: assignment of read-only variable 'PI'
    // MAX_USERS = 200;    // ❌ error: assignment of read-only variable

    // A const can still be USED in calculations -- just not changed:
    double area = PI * 5 * 5;
    cout << "area = " << area << endl;        // area = 78.5398
    return 0;
}

// ✅ Expected output:
//    PI = 3.14159
//    max users = 100
//    area = 78.5397

2. constexpr: Compile-Time Constants

constexpr is a promise that a value can be computed at compile time. That unlocks uses where C++ requires a compile-time constant — most notably the size of a fixed array or a std::array<T, N>. Every constexpr is implicitly const, but it adds the stronger guarantee of compile-time evaluation.

#include <iostream>
#include <array>
using namespace std;

int main() {
    // constexpr = a value known at COMPILE time (stronger than const).
    constexpr int SIZE = 10;
    constexpr double GRAVITY = 9.81;

    // Because SIZE is a compile-time constant, it can size an array:
    int data[SIZE];                 // OK: SIZE is constexpr
    array<int, SIZE> grid{};        // OK too

    for (int i = 0; i < SIZE; ++i) data[i] = i * i;
    cout << "data[9] = " << data[9] << endl;  // data[9] = 81
    cout << "grid size = " << grid.size() << endl; // grid size = 10
    cout << "gravity = " << GRAVITY << endl;       // gravity = 9.81
    return 0;
}

// ✅ Expected output:
//    data[9] = 81
//    grid size = 10
//    gravity = 9.81

Your turn. Choose the right keyword for each constant — one needs to size an array (so it must be compile-time):

#include <iostream>
using namespace std;

int main() {
    // 🎯 YOUR TURN — declare two constants the right way.

    // 1) A run-time-safe read-only tax rate of 0.2:
    ___ double TAX_RATE = 0.2;     // 👉 const

    // 2) A compile-time constant board size of 8 (used to size an array):
    ___ int BOARD = 8;             // 👉 constexpr

    int squares[BOARD];            // needs BOARD to be compile-time
    double price = 50.0;
    cout << "tax = " << price * TAX_RATE << endl;  // ✅ tax = 10
    cout << "cells = " << BOARD * BOARD << endl;   // ✅ cells = 64
    return 0;
}

🧩 Reorder Challenge

These lines use a constexpr size to build and fill an array. Put them in order:

A.     int data[N];
B.     constexpr int N = 4;
C. int main() {
D.     for (int i = 0; i < N; ++i) data[i] = i;
E.     cout << data[3] << endl;
F.     return 0;
G. }

Correct order: C → B → A → D → E → F → G

Open main (C), declare the constexpr size first (B), use it to size the array (A), fill it (D), print data[3] = 3 (E), then return 0; (F) and close (G). N must be a compile-time constant declared before it sizes the array.

3. const References in Parameters

A huge practical use of const is in function parameters. Passing a large object by value copies it; passing by const& hands over a reference (no copy) while promising the function won't modify it. This pattern — const std::string&, const std::vector<T>& — is everywhere in good C++ code: fast and safe.

#include <iostream>
#include <string>
#include <vector>
using namespace std;

// Pass big objects by CONST REFERENCE: no copy, and read-only.
double averageLength(const vector<string>& words) {
    if (words.empty()) return 0.0;
    int total = 0;
    for (const string& w : words) total += w.size();
    return static_cast<double>(total) / words.size();
}

int main() {
    vector<string> names = {"Ann", "Beatrice", "Cy"};
    // 'names' is NOT copied here -- it's passed by const reference.
    cout << "avg length = " << averageLength(names) << endl; // avg length = 4.66667
    return 0;
}

// ✅ Expected output:
//    avg length = 4.33333

4. const Member Functions

A member function with const after its parameter list promises not to modify the object it's called on. Getters are the classic example. The payoff: a const object may only call const member functions, so marking your read-only methods const lets them work on constants and documents that they don't mutate state.

#include <iostream>
#include <string>
using namespace std;

struct BankAccount {
    string owner;
    double balance;

    // const member function: promises NOT to modify the object.
    double getBalance() const { return balance; }     // read-only

    // non-const: it CHANGES the object, so it can't be const.
    void deposit(double amount) { balance += amount; }
};

int main() {
    BankAccount acc{"Mia", 100.0};
    acc.deposit(50.0);
    cout << "balance: " << acc.getBalance() << endl;  // balance: 150

    // A const object can ONLY call const member functions:
    const BankAccount frozen{"Leo", 500.0};
    cout << "frozen: " << frozen.getBalance() << endl; // frozen: 500
    // frozen.deposit(10);   // ❌ error: can't modify a const object
    return 0;
}

// ✅ Expected output:
//    balance: 150
//    frozen: 500

🔎 Deep Dive: constexpr functions

A constexpr function is one the compiler can evaluate at compile time when its arguments are themselves constant. The same function still works at run time when given runtime values — you write it once and get both:

#include <iostream>
using namespace std;

// A constexpr FUNCTION can run at compile time OR run time.
constexpr int square(int n) { return n * n; }

constexpr long long factorial(int n) {
    return (n <= 1) ? 1 : n * factorial(n - 1);
}

int main() {
    // Evaluated at COMPILE time (the result is baked into the binary):
    constexpr int nine = square(3);
    constexpr long long f5 = factorial(5);
    int lookup[square(4)];        // size 16, computed at compile time

    cout << "square(3) = " << nine << endl;      // square(3) = 9
    cout << "factorial(5) = " << f5 << endl;     // factorial(5) = 120
    cout << "array size = " << sizeof(lookup)/sizeof(int) << endl; // 16

    // Same function, RUN-time argument -> still works:
    int x = 7;
    cout << "square(x) = " << square(x) << endl; // square(x) = 49
    return 0;
}

// ✅ Expected output:
//    square(3) = 9
//    factorial(5) = 120
//    array size = 16
//    square(x) = 49

Computing values at compile time means zero runtime cost and lets results be used where the language demands a constant. In C++20, consteval forces compile-time evaluation, and constinit guarantees compile-time initialization of globals.

Pro Tips

🧠 Quick Recall

Predict the output (or whether it compiles) before revealing the answer.

1. Does this compile?

const int LIMIT = 5;
LIMIT = 10;
cout << LIMIT;

No — LIMIT is const, so LIMIT = 10; is an error: "assignment of read-only variable".

2. Does this compile?

int n = 4;
int arr[n];     // is this OK?
constexpr int m = 4;
int brr[m];     // and this?

In standard C++, int arr[n]; with a non-const n is not allowed (variable-length arrays aren't standard). int brr[m]; with constexpr m is fine — it's a compile-time constant.

3. Does the last line compile?

struct C { int v; int get() const { return v; } };
const C c{7};
cout << c.get();   // ?

Yes — prints 7 — get() is a const member function, so a const object may call it.

Common Errors (and the fix)

📋 Quick Reference

TaskCode
Read-only valueconst double PI = 3.14159;
Compile-time constantconstexpr int SIZE = 10;
Read big paramvoid f(const string& s)
Read-only methodint get() const { ... }
constexpr functionconstexpr int sq(int n){...}
const pointer-to-constconst int* const p

Mini-Challenge: Circle Calculator

Put a constexpr constant and const-correct functions together to compute a circle's area and circumference.

#include <iostream>
using namespace std;

// 🎯 MINI-CHALLENGE: Circle calculator with constants
// 1. Declare a constexpr double PI = 3.14159.
// 2. Write a const-correct function:
//      double area(double radius)  -> PI * radius * radius
//    Pass radius however you like (double is cheap to copy).
// 3. Make a const double r = 5.0 in main and print the area.
// 4. BONUS: write circumference(double r) -> 2 * PI * r and print it.
//
// ✅ Example output:
//    area = 78.5398
//    circumference = 31.4159

int main() {
    // your code here
    return 0;
}

🎉 Lesson Complete

Practice quiz

What does 'const' do to a variable?

  • Makes it global
  • Forces compile-time evaluation
  • Marks it read-only so it cannot be reassigned after initialization
  • Makes it faster

Answer: Marks it read-only so it cannot be reassigned after initialization. const is read-only once set; any attempt to reassign it is a compile error.

What is the key difference between const and constexpr?

  • constexpr requires a value computable at compile time; const may be computed at run time
  • No difference
  • const is compile-time, constexpr is run-time
  • constexpr can be reassigned

Answer: constexpr requires a value computable at compile time; const may be computed at run time. Both can't be reassigned, but constexpr guarantees compile-time evaluation; const only guarantees read-only.

Which can be used as the size of a fixed-size array?

  • Any const
  • Any runtime int
  • Only literals
  • A constexpr value (or a const that is itself a compile-time constant)

Answer: A constexpr value (or a const that is itself a compile-time constant). Array sizes require a compile-time constant; constexpr guarantees this.

Why pass a large object by const reference (const T&)?

  • To copy it safely
  • To avoid copying while promising not to modify it
  • To make it mutable
  • To force compile-time evaluation

Answer: To avoid copying while promising not to modify it. const& gives the speed of passing by reference (no copy) and the safety of read-only access.

What does marking a member function 'const' (after its parameter list) promise?

  • It will not modify the object it is called on
  • It returns a const value
  • It is evaluated at compile time
  • It is private

Answer: It will not modify the object it is called on. A const member function promises not to mutate the object, so const objects may call it.

A const object can call which member functions?

  • Any member function
  • Only static functions
  • Only const member functions
  • None

Answer: Only const member functions. A const object may only call const member functions, which is why getters should be marked const.

Does 'const int LIMIT = 5; LIMIT = 10;' compile?

  • Yes
  • No — 'assignment of read-only variable'
  • Only with -std=c++20
  • Only if LIMIT is global

Answer: No — 'assignment of read-only variable'. Reassigning a const is a compile error.

Can a single constexpr function be called at run time too?

  • No, never
  • Only if marked inline
  • Only with consteval
  • Yes — it runs at compile time when its arguments are constant, and at run time otherwise

Answer: Yes — it runs at compile time when its arguments are constant, and at run time otherwise. A constexpr function works in both modes depending on whether its arguments are compile-time constants.

Why does 'constexpr int x = readInput();' fail to compile?

  • readInput is private
  • The initializer must be a constant expression, but a runtime value isn't known at compile time
  • constexpr needs a double
  • x must be const

Answer: The initializer must be a constant expression, but a runtime value isn't known at compile time. constexpr demands a compile-time-known initializer; use plain const for a runtime value.

Every constexpr variable is also:

  • static
  • inline
  • const
  • volatile

Answer: const. constexpr implies const and adds the stronger guarantee of compile-time evaluation.

Continue this course

Frequently asked questions

What does const mean in C++?

const marks something as read-only: once it is set, you cannot reassign it. The compiler enforces this, so any attempt to modify a const value is a compile error. It is used for values, function parameters, references, and member functions.

What is the difference between const and constexpr?

const means the value cannot change after initialization, but it may be computed at run time. constexpr is stronger: the value must be computable at compile time, so the compiler can bake it in and use it where a compile-time constant is required, such as an array size.

Why should I pass parameters by const reference?

Passing a large object by const reference (const T&) avoids copying it while promising the function will not modify it. You get the speed of passing by reference and the safety of read-only access.

What is a const member function?

A member function declared const after its parameter list promises not to modify the object it is called on. This lets you call it on const objects and clearly documents that it is a read-only operation, like a getter.

Can a constexpr function be called at run time too?

Yes. A constexpr function runs at compile time when its arguments are constant expressions, and at run time otherwise. So you can use the same function for both compile-time constants and ordinary runtime values.

Related lessons