const correctness — const refs, const methods & when to use
Scenario
You pass a large std::string to a function by reference to avoid copying it. Another developer calls your function with a string literal, but the compiler throws an error saying 'cannot bind non-const lvalue reference'.
How do you promise the compiler that you're just looking at the data, not changing it?
Mental model
const is a legally binding contract with the compiler. It means 'I promise not to mutate this.' If you try to break the promise, the code refuses to compile.
const correctness is the practice of using the `const` keyword proactively to prevent unintended state changes. Passing a large object by `const std::string&` avoids a copy AND promises not to change it. A `const` method (e.g., `int getSize() const`) promises not to modify any member variables of the class. This creates a chain of trust: const objects can only call const methods.
Explanation
When you pass a large object to a function by value, C++ copies it. To avoid the copy, you pass it by reference. However, a plain reference (`&`) allows the function to modify the object, and crucially, you cannot bind a plain reference to a temporary object (like a string literal or a function return value).
The fix is `const Type&`. It gives you the performance of a reference with the safety of a copy. It also allows binding to temporary values.
Inside a class, putting `const` at the end of a method signature (e.g., `void print() const;`) marks the method itself as constant. This tells the compiler that calling this method won't change the object's state. If you have a `const` object, you can ONLY call `const` methods on it. This is why getters should always be marked `const`.
When combining `const` with pointers, read the declaration from right to left to understand what is constant. For example, `const int* ptr` (or `int const* ptr`) means 'pointer to an integer that is constant'—you can change where the pointer points, but you cannot modify the integer it points to. Conversely, `int* const ptr` means 'constant pointer to an integer'—you can modify the integer, but you cannot change where the pointer points. `const int* const ptr` combines both: a constant pointer to a constant integer.
Code examples
The bug: Missing const in parameters and methods
#include <string>
#include <iostream>
class User {
std::string name;
public:
User(std::string n) : name(n) {}
// Missing const!
std::string getName() { return name; }
};
// Takes a plain reference
void printName(User& u) {
std::cout << u.getName() << '\n';
}
int main() {
// This fails to compile! A temporary User cannot bind to a non-const reference.
printName(User("Alice"));
return 0;
}
`User("Alice")` is a temporary object (an rvalue). Plain lvalue references (`User&`) cannot bind to temporaries because modifying a temporary makes no sense. The code is broken.
The fix: const correctness
#include <string>
#include <iostream>
class User {
std::string name;
public:
User(std::string n) : name(n) {}
// Marked const: promises not to modify members
std::string getName() const { return name; }
};
// Takes a const reference: binds to temporaries and avoids copying
void printName(const User& u) {
std::cout << u.getName() << '\n'; // Allowed because getName() is const
}
int main() {
printName(User("Alice")); // Works perfectly!
return 0;
}
By changing the parameter to `const User&`, we can pass temporaries. However, inside `printName`, `u` is `const`. The compiler will only let us call `u.getName()` if `getName()` is explicitly marked `const`.
Key points
- Const as a contract: Use const proactively. If a variable shouldn't change, mark it const. If a parameter is read-only, use const&.
- Const methods: If a method doesn't modify state, mark it const so it can be called on const instances.
Common mistakes
- Forgetting const on getter methods: If a getter isn't marked `const`, you won't be able to call it when the object is passed into a function as a `const&`.
Recall questions
- Why is pass-by-const-reference preferred for large objects over pass-by-value?
- What does placing 'const' at the end of a class method declaration mean?
- Why must you use const& if you want to pass a temporary object (like a string literal) by reference?
Questions & answers
A C++ program has a `const std::vector<int>& data` parameter. Inside the function, the programmer tries to sort the vector. What happens and why?
The code will not compile. The `const` keyword acts as a contract that the object cannot be mutated. `std::sort` requires modifying the container's elements, which violates the `const` qualifier.
Approach: Understand that const is enforced at compile time, rejecting any mutating operations.
In an interview, you see `const int* const ptr`. What does this mean?
It means a constant pointer to a constant integer. The integer cannot be modified through the pointer, and the pointer itself cannot be reassigned to point to another integer.
Approach: Read declarations from right to left: 'const pointer to const int'.
Continue learning
Previous: References vs pointers — syntax & mental model