Constants and Modifiers — const, constexpr, consteval, volatile, mutable
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
- What is the difference between
const int&andint const&? - Can a
constexprfunction have side effects? Explain. - When would you use
volatileinstead ofstd::atomic? - Why is
mutablenecessary for some caching implementations? - Write a
constevalfunction 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
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