Hello! Welcome to your fifth lesson on C++ internals.
In our last lesson, we explored function overloading and the compiler's detailed algorithm for overload resolution. We saw how the compiler ranks different argument conversions to find the "best" function for a call. We concluded with a promise to investigate how the const keyword fits into this picture.
Today, we deliver on that promise. We'll explore const correctness, a fundamental pillar of modern C++. You'll learn that const is far more than a way to declare a fixed number; it's a powerful contract you make with the compiler and other programmers. We will analyze its syntax, its role in designing safe and readable APIs, and, critically, how this contract affects the code your compiler generates and the optimizations it can perform.
const as a Promise, Not Just a Value
In C, you're familiar with using #define to create symbolic constants. C++ offers const for this, but its role is much broader and more profound. At its heart, const is a promise that a piece of data will not be modified. This allows the compiler to turn potential runtime bugs (accidental modifications) into compile-time errors.
Let's start with a foundational video by Dan Saks from CppCon, which frames the entire concept of const as a system of promises.
Back to Basics: Const as a Promise - Dan Saks - CppCon 2019
This video, 'Back to Basics: Const as a Promise', is one of the best explanations available. We'll start with the introductory segment to set the stage.
Watch from 01:02 to 02:07. This section introduces the core idea that const's main utility in modern C++ is to prevent data modification, improving code hygiene and safety.
This "promise" has two key audiences:
- Other Programmers (and yourself): When they see
constin a function signature, they know it's safe to pass their data without fear of it being changed. It's a form of documentation enforced by the compiler. - The Compiler: By promising that a value won't change, you give the compiler information it can use to reason about your code, which can sometimes lead to better optimizations.
Level 1: const with Pointers - The Rules of the Road
The interaction between const and pointers is where the rules get specific and powerful. This is a common point of confusion, but it's perfectly logical if you read the declaration from right to left.
Let's watch a key segment from Dan Saks's talk that breaks this down.
Back to Basics: Const as a Promise - Dan Saks - CppCon 2019
This section of the 'Const as a Promise' video provides a clear, syntactic breakdown of how const applies to pointers.
Watch from 19:17 to 26:25. Pay close attention to the distinction between a pointer-to-const, a const-pointer, and a const-pointer-to-const. Try to internalize the 'right-to-left' reading rule.
To summarize the rules from the video:
const T* porT const* p: Pointer toconst T.- The pointer
pcan be changed to point elsewhere. - The data pointed to by
p(*p) cannot be modified through this pointer.
- The pointer
T* const p:constpointer toT.- The pointer
pis constant and cannot be changed to point elsewhere. It must be initialized where it is declared. - The data pointed to by
p(*p) can be modified.
- The pointer
const T* const p:constpointer toconst T.- Neither the pointer
pnor the data it points to (*p) can be changed.
- Neither the pointer
This distinction is crucial. A "pointer-to-const" (const T*) does not mean the underlying object is immutable. It only means you are promising not to change it through that specific pointer.

The Ethical Framework of const
Why does C++ allow you to assign a T* to a const T*, but not the other way around? The compiler enforces a kind of "ethical" logic based on promises.
Back to Basics: Const as a Promise - Dan Saks - CppCon 2019
Let's continue with the Dan Saks video to understand this core principle.
Watch from 26:25 to 34:43. The analogy of borrowing an item and promising not to change it is the key mental model here. This explains 'qualification conversions'—why adding const is safe, but removing it is forbidden without an explicit cast.
This leads to the fundamental rule of const correctness:
- Adding
constis safe and implicit: You can always make a stronger promise. Assigning aT*to aconst T*is fine. You're taking a modifiable object and promising not to modify it through this new pointer. - Removing
constis unsafe and requires an explicit cast (const_cast): You cannot implicitly break a promise. Assigning aconst T*to aT*would create a loophole, allowing you to modify data that was supposed to be protected.
This same logic applies to references (T& vs. const T&) and even iterators in the Standard Library (iterator vs. const_iterator).
Level 2: const Member Functions
Now, let's connect this to our previous lesson on overloading. const allows you to create two versions of a member function: one for const objects and one for non-const objects.
class MyData {
int value;
public:
int getValue() const { // The 'const' promise
return value;
}
void setValue(int v) {
value = v;
}
};
const MyData data;
int v = data.getValue(); // OK: getValue() is a const member function
// data.setValue(42); // COMPILE ERROR: cannot call non-const member function on a const object
How does this work "under the hood"?
When you add const after a member function's parameter list, you are changing the type of the hidden this pointer.
- In a non-
constmember function,thishas the typeMyData*. - In a
constmember function,thishas the typeconst MyData*.
This means that inside a const member function, all member variables are treated as const, preventing you from modifying the object's state.
This mechanism also allows you to overload on const:
class MyArray {
public:
// For non-const objects: returns a modifiable reference
int& at(int index) { return data[index]; }
// For const objects: returns a non-modifiable reference
const int& at(int index) const { return data[index]; }
private:
int data[10];
};
MyArray arr;
arr.at(0) = 42; // Calls non-const version, OK
const MyArray const_arr = arr;
// const_arr.at(0) = 99; // COMPILE ERROR: at() returns const int&
int val = const_arr.at(0); // Calls const version, OK
The compiler uses the const-ness of the object (arr vs. const_arr) as part of its overload resolution algorithm to pick the correct version of at().
Test your understanding!
You are designing a simple Vector class that holds a pointer to dynamically allocated data. Which of the following member functions should be marked const?
size(): returns the number of elements.push_back(int value): adds an element to the end.capacity(): returns the allocated capacity.operator[](int index): returns a reference to an element.clear(): removes all elements.
Show answer
The functions that should be const are:
size(): This function only reads the size member; it doesn't modify the logical state of the vector. You should be able to ask for the size of aconstvector.capacity(): Similar tosize(), this only reads a member variable and doesn't modify the vector.operator[](int index)(theconstoverload): As we saw in the example, you should provide aconstversion of this operator that returns aconst int&. This allows read-only access to elements of aconstvector.
The functions push_back() and clear() clearly modify the state of the vector and cannot be const. The non-const overload of operator[] also modifies state by returning a modifiable reference, so it cannot be const.
Level 3: const and Compiler Optimization - The Myth and the Reality
A common belief is that liberal use of const makes your code faster. The reality is more nuanced. const is primarily for correctness. The optimization benefits are a conditional side effect.
When const Can Help the Compiler
When the compiler can be certain that an object is truly constant, it can perform powerful optimizations.
What kind of optimization does const offer in C/C++?
This Stack Overflow answer provides excellent, concrete assembly examples of optimizations enabled by const.
Read 'Example 1 - Const enables constant folding' and 'Example 2 - Const prevents clobbering'. Focus on how the assembly code changes. In Example 1, a generic powf call becomes a specialized and faster exp2f call. In Example 2, the compiler avoids a memory read after a function call, instead hardcoding the return value to 0.
These optimizations are possible because the const declaration on a local variable gives the compiler a strong guarantee. The compiler knows that any attempt to modify it is Undefined Behavior, and it can assume Undefined Behavior never happens.
The Undefined Behavior of Modifying a const Object
Your knowledge of OS memory protection gives you a great advantage in understanding what happens when you try to break this promise. Modifying an object that was originally defined as const is Undefined Behavior.
Clarifying about literals, macros, and const (still not constant?)
This video from Jacob Sorber demonstrates what often happens when you try to modify a const variable. It's a perfect example of 'under the hood' behavior.
Watch from 05:18 to 12:51. This segment shows two things: First, trying to modify a const variable often results in a segmentation fault because the compiler places it in a read-only memory page. Second, even if you bypass this with mprotect, the compiler might have already optimized away the memory read, substituting the value directly into the assembly. This is a fantastic real-world demonstration of const affecting code generation.
When const Doesn't Help the Compiler
The major caveat is aliasing. When you pass a pointer-to-const (const T*) to a function, the compiler usually can't be sure that there isn't another, non-const pointer (T*) pointing to the same memory somewhere else in the program.
Why const Doesn't Make C Code Faster
For a balanced perspective, let's look at this article from 'The Art of Machinery' which argues that const is often ineffective for optimization in C.
First, read the section 'Something that (sort of) works' to see the critical difference between a truly const local object and a const pointer to a potentially non-const object. Then, read the 'TL;DR' summary and the final section 'So, what’s const for?'. This reinforces that the primary benefit is type safety and correctness, not performance.
The takeaway is that you should not use const with the primary goal of optimization. Use it to make your interfaces safer, your code more readable, and your intentions clearer.
Conclusion
Today we've seen that const is a cornerstone of writing robust C++. It's a contract that operates at multiple levels, from function signatures down to memory protection flags managed by the OS.
Key Takeaways:
constis a Promise: Its primary purpose is to declare an intent not to modify data, which the compiler enforces. This improves code safety and readability.- Pointer
const-ness is Specific: You must distinguish between aconstpointer and a pointer-to-const. The "read it right-to-left" trick is your best tool here. constMember Functions: A member function markedconstpromises not to modify the object's state. This is enforced by changing the type of the hiddenthispointer toconst MyClass*, and it allows forconst-overloading.- Optimization is a Side Effect, Not a Goal: While
constcan enable optimizations like constant folding when an object is truly immutable, it often doesn't in the presence of pointers and references due to aliasing. The main benefit remains correctness.
In our next lesson, we will cover namespaces. While const helps manage how data is accessed, namespaces help manage how names are accessed, providing a crucial tool for organizing large codebases and preventing the C problem of name collisions at the linker level.