AlgoMaster Logo

const Correctness

High Priority18 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

const is one of C++'s strongest tools for catching bugs before the program even runs. It lets you tell the compiler, "this value should not change", and the compiler enforces that promise everywhere the value flows. Sprinkling const everywhere it belongs is what experienced C++ developers call being "const-correct," and it changes how solid a codebase feels.

Why const Correctness Matters

A const declaration does four things at once.

It catches bugs at compile time. If you accidentally write to a value that should not change, the compiler stops you. That feedback is free and instant, and it beats finding the same bug in production.

It documents intent. When a function takes const std::string& name, the signature promises the function will not modify name. Future readers do not need to read the body to be sure.

It enables compiler optimizations. The compiler can sometimes keep const values in registers, fold them into call sites, or avoid reloading them after an opaque function call. The wins are usually small, but they are free.

It hints at thread safety. A value that no thread can modify is a value no thread can race on. const does not make a function thread-safe by itself, but it removes one entire class of race condition: the write-write and write-read collisions.

The smallest possible demo:

That single const means nothing later in the program, no matter how big it grows, can change the tax rate. The compiler refuses.

const on Function Parameters

The most common use of const is on function parameters. The choice comes down to pass-by-value vs pass-by-const-reference, and the rule is simpler than it looks.

For cheap-to-copy types (int, double, bool, small enums, pointers), pass by value. The copy is one machine instruction, and pass-by-value avoids an extra level of indirection.

For non-trivial types (std::string, std::vector, classes with allocated members), pass by const reference. You skip the copy entirely while still protecting the caller's object.

Passing a std::vector<std::string> by value copies every string, which means a heap allocation per element. A const reference is one pointer-sized argument.

Inside printCart, the loop variable item is also a const reference. Same reasoning: no copy per element, and the compiler stops anyone from writing to the cart through the loop.

Should you put const on a by-value parameter, like void f(const int x)? You can, and it is harmless inside the function body. But it is not part of the signature the caller sees, so it neither helps nor hurts API design. Most style guides drop it.

const on Return Types

Return types are where const gets interesting, because the right choice depends on whether you return by value, reference, or pointer.

Return by value: Do not put const on the return type. A const return-by-value is mostly useless and actively harmful in modern C++.

Returning const std::string prevents the caller from moving out of the temporary, which kills an important optimization. The C++ Core Guidelines call this out specifically.

Return by reference or pointer: Use const to say "you can look at this, but you cannot modify it." This is the right pattern for getters that expose internal state.

The const std::vector<std::string>& return type lets callers iterate cheaply (no copy) while protecting the Cart from being modified through the back door.

const Member Functions

A const member function is one that promises not to modify the object it is called on. The syntax puts const after the parameter list.

Inside printProduct, p is a const Product&. You can only call methods marked const on it. If getPrice() were not const, the line p.getPrice() would fail to compile. That is the chain that gives const correctness its power: one missing const deep in a class breaks every read-only path through the program.

The diagram shows how const propagates: once an object becomes const (through a reference, pointer, or the object being declared const), every method call goes through this same check.

The const / non-const Overload Pair

For accessors that return a reference into an object, you often need two versions of the same getter: one for const objects (returns a const reference), one for non-const objects (returns a mutable reference).

The compiler picks the right overload based on whether the object itself is const. This pattern shows up everywhere in the standard library. std::vector::operator[] has this exact pair of overloads.

const Data Members

A data member marked const must be initialized at construction time and can never be reassigned afterward. The only place to initialize it is the constructor's member initializer list.

The trade-off is real: const data members block the compiler-generated copy assignment and move assignment operators, because both would need to overwrite the const field. If you need to copy or assign these objects later, prefer private non-const members with no setter. Same protection, none of the assignment headaches.

const data members disable implicit move-assignment, which can turn cheap moves into expensive copies elsewhere in the code. Use them when the immutability guarantee outweighs the assignment loss.

const Pointers vs Pointer to const

Pointer syntax with const is a frequent source of confusion. There are three combinations.

DeclarationWhat is constCan you change the pointer?Can you change the pointed-to value?
int* pnothingyesyes
const int* pthe pointed-to datayesno
int* const pthe pointer itselfnoyes
const int* const pbothnono

Read declarations right-to-left.

  • const int* p reads as "p is a pointer to int const", so the int is const, the pointer is not.
  • int* const p reads as "p is a const pointer to int", so the pointer is const, the int is not.

Const correctness pushes you to use const T* for any pointer that does not need write access, the same way you would use const T&.

East const vs West const

Two styles appear in real codebases.

Both are identical to the compiler. The arguments for east const are consistency and readability: const always modifies what is on its left, so the right-to-left reading rule never breaks down. With west const, the leading const is a one-off exception because there is nothing to its left.

Pick one and stick with it. Most codebases still use west const because that is what the language tutorials use, and the C++ Core Guidelines do not take a side. Pick whichever your team uses.

const Iterators

Containers have two ways to give you a "const view" through iterators.

The first is cbegin() and cend(), added in C++11. These return const_iterator, regardless of whether the container itself is const. You are telling the loop "I want read-only iteration."

The second is calling begin() on a const container. Because the container is const, you get a const_iterator back via the overload pair described earlier.

A subtle distinction worth knowing. const_iterator is "iterator-to-const": the iterator can move, but *it = ... is forbidden. A const iterator (note the position of const) is the opposite: the iterator itself cannot be incremented or reassigned, but you could still write through it. In practice you almost always want const_iterator, which is what cbegin/cend give you.

How const Propagates

const does not stop at the boundary you put it on. It flows through every method call, every parameter passed, every reference taken.

Start with one const Cart&. The compiler picks const overloads of its methods. Each const method only sees its own members as const. If one of those methods passes a member by reference to a helper, the helper must accept it as const T&, or the call fails. The "const-ness" pours through the whole call graph.

The diagram traces one entry point: a const Cart& argument. Everything reachable through const methods stays const all the way down. Anything that would break the promise, like addItem, is blocked at the entry.

One subtle point: const applies to the object the reference or pointer binds to, not to anything reached through other indirection. A const std::vector<int*> blocks you from changing which int* lives at each slot, but the int values that those pointers reach can still be modified, because the pointers stored in the vector are not themselves const int*. This is sometimes called shallow const.

To forbid mutation of the pointed-to values, store const int* (or const int* const).

mutable: The Escape Hatch

Sometimes you want a const method to modify one specific member. Caches, mutex locks, hit counters, and lazy initialization all fall into this category. The mutable keyword carves out an exception: a member declared mutable can be modified even from a const method.

The rule of thumb: mutable is for members that do not affect the logical state of the object. A cache field, a usage counter, a mutex used to make the const method thread-safe. Using mutable to mutate something the user cares about is misuse.

constexpr Is Not const

const and constexpr are different.

  • const means "this value will not be changed by this code". The value might still be computed at runtime, and the compiler often cannot fold it away.
  • constexpr means "this value can be computed at compile time". A constexpr variable is also implicitly const, but it carries the stronger guarantee that its initializer is a compile-time constant.

You can put constexpr on functions and variables that can be evaluated at compile time. For everyday code, use const for "don't change this" and reach for constexpr only when you actually need compile-time evaluation (for example, sizing an array or supplying a non-type template argument).

Common const Bugs

Some const-related bugs come up over and over. Recognizing the shape of each one cuts review time in half.

What is wrong with this code?

getName() is not marked const, so it cannot be called through a const Product&. The signature std::string& getName() also exposes the internals mutably, defeating encapsulation.

Fix:

Add const after the parameter list and return a const std::string&. Now printName compiles, and no caller can rewrite the name through the getter.

What is wrong with this code?

getItems() is const, but the return type is std::vector<std::string> by value. Every call copies the entire cart. For a cart with thousands of items, this is slow without showing up as an obvious cost in the code.

Fix:

Return by const reference. No copy, no write access.

What is wrong with this code?

price is a const double&, so writing to it is illegal. The error message from g++ is clear: assignment of read-only reference.

Fix: decide which parameter is input and which is output, and stop conflating them. price should stay const; result is the only one that is allowed to change.

Quiz

Const Correctness Quiz

10 quizzes