Skip to content

Constants and Modifiers — const, constexpr, consteval, volatile, mutable

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Constants and Modifiers. We cover key concepts, practical examples, and best practices to help you master this topic.

C++ provides five type modifiers that control how and when values are computed and stored, giving programmers fine-grained control over correctness, performance, and optimization boundaries.

What You'll Learn

You will understand the difference between const (runtime immutability), constexpr (compile-time evaluation), consteval (immediate functions), volatile (preventing optimization around hardware interactions), and mutable (modifying parts of const objects). You will also learn when each modifier is appropriate and how they interact.

Why It Matters

Constants and modifiers are not just about making values unchangeable. They communicate intent to both the compiler and other developers. constexpr enables computation at compile time, reducing runtime work. volatile is essential for embedded and systems programming. mutable solves real problems with logical constness in Caching and reference counting.

Learning Path

graph LR
    A["04: Variables & Types"] --> B["05: Constants & Modifiers"]
    B --> C["06: Operators"]
    C --> D["07: Control Flow"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

const — Runtime Immutability

A const object cannot be modified after initialization. The compiler enforces this at compile time.

#include <iostream>

int main() {
    const int MAX_USERS = 100;
    // MAX_USERS = 200;  // Error: assignment to const
    
    const double PI = 3.1415926535;
    // PI = 3.0;  // Error
    
    std::cout << MAX_USERS << " " << PI << "\n";
    
    // const with pointers
    int x = 10;
    const int* p = &x;     // pointer to const int (cannot modify through p)
    int* const q = &x;     // const pointer to int (cannot change q itself)
    const int* const r = &x;  // const pointer to const int
    
    // *p = 20;  // Error
    // q = &y;   // Error
}

const is primarily a correctness tool. It guarantees that a value does not change, making code easier to reason about. Mark any variable that should not change as const by default.

const Member Functions

Member functions can be declared const, meaning they promise not to modify the object:

class Point {
    double x_, y_;
public:
    Point(double x, double y) : x_(x), y_(y) {}
    
    double x() const { return x_; }  // const member function
    double y() const { return y_; }
    
    void setX(double x) { x_ = x; }  // non-const
};

int main() {
    const Point origin(0, 0);
    // origin.setX(10);  // Error: cannot call non-const on const object
    double ox = origin.x();  // OK
}

constexpr — Compile-Time Evaluation

constexpr tells the compiler that a variable or function can be evaluated at compile time. Unlike const, which only guarantees immutability, constexpr guarantees compile-time evaluation when possible.

#include <iostream>

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

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

int main() {
    constexpr int SIZE = 100;
    constexpr int SQUARE = square(12);       // computed at compile time
    constexpr int FACT = factorial(5);       // computed at compile time
    
    int arr[SQUARE];  // OK: SQUARE is a constant expression
    
    std::cout << SQUARE << " " << FACT << "\n";
    
    // constexpr with runtime values
    int runtime;
    std::cin >> runtime;
    // constexpr int bad = square(runtime);  // Error: not constant
    const int runtime_const = square(runtime);  // OK: computed at runtime
}

A constexpr function can be evaluated at compile time if all arguments are constant expressions, or at runtime otherwise. This gives you a single function that works in both contexts.

constexpr Variables in C++17

C++17 extended constexpr to allow more complex computations:

constexpr int fibonacci(int n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);  // OK in C++17 constexpr
}

constexpr int FIB10 = fibonacci(10);  // compile-time

C++20 further expanded constexpr to allow dynamic allocation, virtual function calls, and try blocks.

consteval — Immediate Functions (C++20)

consteval goes further than constexpr: it requires that the function produces a constant expression. It always executes at compile time.

#include <iostream>

consteval int cube(int n) {
    return n * n * n;
}

int main() {
    constexpr int C = cube(5);    // OK
    int arr[cube(3)];             // OK
    
    int x = 10;
    // int y = cube(x);  // Error: x is not a constant expression
    
    std::cout << C << "\n";
}

Use consteval when a function must always produce a compile-time constant (e.g., for array sizes, template arguments).

volatile — Preventing Optimizations

volatile tells the compiler that a variable may change at any time, outside the program's control (e.g., by hardware, a signal handler, or a different thread). It prevents the compiler from optimizing away reads and writes.

#include <iostream>

int main() {
    volatile int status_register = 0;
    
    // Compiler must read this value every time, cannot cache in register
    while (status_register == 0) {
        // wait for hardware to update the register
    }
    
    std::cout << "Status changed\n";
}

Without volatile, the compiler might optimize the loop to if (status_register == 0) while(true);, which reads the value only once.

Note: volatile does not provide atomicity or synchronization. For multithreading, use std::atomic instead. volatile is primarily for memory-mapped I/O, signal handlers, and setjmp contexts.

mutable — Modifying Parts of Const Objects

mutable allows a member variable to be modified even when the object is const. Use it for internal state that does not affect the observable value, such as caches, reference counts, or mutexes.

#include <iostream>
#include <string>

class DataProcessor {
    std::string data_;
    mutable int access_count_ = 0;
public:
    DataProcessor(std::string d) : data_(std::move(d)) {}
    
    const std::string& getData() const {
        ++access_count_;  // OK: mutable
        return data_;
    }
    
    int accessCount() const { return access_count_; }
};

int main() {
    const DataProcessor proc("secret");
    proc.getData();
    proc.getData();
    proc.getData();
    std::cout << "Accessed " << proc.accessCount() << " times\n";
}

Without mutable, you would need to make access_count_ non-const, which would prevent calling getData on a const object.

Comparing the Modifiers

Modifier When Evaluated Can Change? Typical Use
const Runtime No Immutable values, API contracts
constexpr Compile-time (if possible) No Compile-time constants and functions
consteval Always compile-time No Functions that must be constant
volatile Runtime Yes (externally) Hardware registers, signal handlers
mutable Runtime Yes (internally) Caches, counts, mutexes in const objects

Common Mistakes

Mistake 1: const vs constexpr Confusion

const int x = time();     // OK, runtime constant
constexpr int y = time(); // Error: time() is not constexpr

constexpr requires a compile-time constant initializer. const does not.

Mistake 2: Missing const in Member Functions

Always mark getters and non-mutating functions as const. Failing to do so prevents calling them on const objects and references.

Mistake 3: Using volatile for Thread Synchronization

volatile int flag = 0;  // Wrong for threading

Use std::atomic<bool> instead. volatile does not prevent race conditions or provide memory ordering.

Mistake 4: Overusing mutable

mutable is often misused to bypass const-correctness. Only use it for genuinely internal state that does not affect logical constness.

Mistake 5: Assuming constexpr Functions are Always Compile-Time

A constexpr function can be called at runtime. If you need guaranteed compile-time execution, use consteval (C++20).

Mistake 6: const Pointers vs Pointer to const

int x = 5;
const int* p1 = &x;   // pointer to const int
int* const p2 = &x;   // const pointer to int

Read declarations right-to-left: p1 is a pointer to const int; p2 is a const pointer to int.

Practice Questions

  1. What is the difference between const int& and int const&?
  2. Can a constexpr function have side effects? Explain.
  3. When would you use volatile instead of std::atomic?
  4. Why is mutable necessary for some caching implementations?
  5. Write a consteval function that checks whether a number is prime at compile time.

Challenge

Write a constexpr function that computes the nth Fibonacci number. Then use it to initialize a constexpr variable. Measure whether the computation happens at compile time by trying to use the result as an array size.

FAQ

Is `constexpr` the same as `const`?

No. const means the value cannot change after initialization. constexpr means the value must be computable at compile time. Every constexpr variable is implicitly const, but a const variable is not necessarily constexpr.

Can I use `constexpr` with `std::vector`?

In C++20, yes. constexpr allocations are allowed in constexpr contexts since C++20, but they must be deallocated by the end of constant evaluation.

What happens if `consteval` is called with a runtime value?

The program is ill-formed (compile-time error). consteval functions cannot be called with runtime arguments.

Is `volatile` useful in multithreaded programming?

No. For multithreading, use std::atomic which provides both atomic operations and memory ordering guarantees. volatile only prevents compiler optimizations, not hardware reordering.

Can I use `mutable` with lambdas?

Yes, since C++14, lambdas can declare mutable captures (e.g., mutable keyword after the parameter list). This allows the lambda to modify captured-by-value variables.

Does `const` improve performance?

Sometimes. const enables compiler optimizations like constant propagation and inlining. More importantly, it enables move semantics and prevents accidental mutations.

Mini Project

Write a compile-time math library using constexpr:

#include <iostream>

constexpr double power(double base, int exp) {
    double result = 1.0;
    for (int i = 0; i < (exp >= 0 ? exp : -exp); ++i) {
        result *= base;
    }
    return exp >= 0 ? result : 1.0 / result;
}

constexpr double PI = 3.141592653589793;

int main() {
    constexpr double squared = power(PI, 2);
    constexpr double cubed = power(PI, 3);
    
    std::cout << squared << " " << cubed << "\n";
    
    // Demonstrate consteval equivalence
    int runtime_pow;
    std::cin >> runtime_pow;
    double runtime_result = power(PI, runtime_pow);  // Still works at runtime
    std::cout << runtime_result << "\n";
}

Expected output:

9.8696 31.0063

What's Next

Constants make your code safer and more expressive. The next lesson covers operators: arithmetic, relational, logical, and bitwise. You will learn how to manipulate values and combine expressions.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro