Skip to content
C++
Language
since C++98
Intermediate

Const Correctness

Applying const to variables, references, pointers, and member functions to express and enforce immutability contracts throughout a C++ codebase.

Const Correctnesssince C++98

Const correctness is the discipline of marking every entity—variable, parameter, reference, pointer, and member function—with const wherever mutation is neither intended nor required, turning accidental writes into compile-time errors.

Overview

const is a promise to both the compiler and to readers: this entity will not be modified through this handle. When applied consistently, mutation errors surface at compile time rather than as subtle runtime bugs, and they unlock important optimisations—the compiler can eliminate redundant loads, inline constant values, and avoid unnecessary copies.

Const correctness is not optional hygiene. Code that lacks it accumulates silent mutation paths, prevents objects from being passed to const-correct APIs, and erodes clarity about intent. Retrofitting it into an existing codebase is painful: a single missing const propagates upward through the call chain because a non-const reference cannot bind to a const object, forcing callers to change signatures too.

Syntax

Variables and objects

cpp
const int max_retries = 5;
const std::string prefix = "GET /";  // object is fully immutable after construction

Pointers: two orthogonal axes

cpp
const int* p1 = &x;       // pointer to const int — *p1 is read-only, p1 itself can change
int* const p2 = &x;       // const pointer to int — p2 cannot be reseated, *p2 can change
const int* const p3 = &x; // both the pointer and the pointee are const

Read right-to-left: const int* const = "const pointer to const int."

References

cpp
void inspect(const std::string& s);  // const ref: no copy, no mutation
void modify(std::string& s);         // non-const ref: mutation expected

A non-const reference cannot bind to a const object or a temporary—the compiler enforces this:

cpp
const std::string greeting = "hello";
std::string& bad = greeting;           // error: drops const qualifier
const std::string& ok = greeting;      // fine
const std::string& temp = get_name();  // fine: const ref extends lifetime of the temporary

Const member functions

cpp
class Rectangle {
public:
    double area() const;        // const member function: cannot modify *this
    void   scale(double factor); // non-const: may modify *this
private:
    double width_, height_;
};

The const qualifier goes after the parameter list. Placing it before would qualify only the return type, not the function itself.

Only const member functions may be called on a const object or through a const reference. When defined outside the class, the suffix must appear on the definition as well:

cpp
double Rectangle::area() const {   // 'const' required here too
    return width_ * height_;
}

Examples

Passing large objects without copying

cpp
#include <vector>

double average(const std::vector<double>& data) {
    if (data.empty()) return 0.0;
    double sum = 0.0;
    for (double v : data) sum += v;
    return sum / static_cast<double>(data.size());
}

const& avoids copying a potentially large container and documents that average will never alter the caller's data. For cheap scalar types (int, double, char), there is no benefit—prefer pass-by-value there.

Const and non-const overloads

A class often needs both a read path and a write path for the same accessor:

cpp
class Matrix {
public:
    const double& operator()(int r, int c) const; // read path
    double&       operator()(int r, int c);        // write path
private:
    std::vector<double> data_;
    int rows_, cols_;
};

The compiler selects the const overload when the receiver is const-qualified. Implement the non-const overload in terms of the const one to avoid duplication (pre-C++23 idiom):

cpp
// C++17 and earlier: cast-based deduplication
double& Matrix::operator()(int r, int c) {
    return const_cast<double&>(
        static_cast<const Matrix&>(*this)(r, c)
    );
}

C++23 introduced deducing this, which eliminates this pattern entirely:

cpp
// C++23
auto& operator()(this auto& self, int r, int c) {
    return self.data_[r * self.cols_ + c];
}

mutable: controlled escape hatch

A member declared mutable may be modified inside a const member function. This is appropriate for bookkeeping state that is not part of the object's logical value:

cpp
#include <mutex>
#include <optional>

class ExpensiveValue {
public:
    double get() const {
        std::lock_guard lock(mutex_);       // C++17 CTAD
        if (!cached_) cached_ = compute();
        return *cached_;
    }
private:
    double compute() const;

    mutable std::mutex           mutex_;   // synchronisation — not domain state
    mutable std::optional<double> cached_; // lazy cache — C++17
};

Reserve mutable for caches, mutexes, and reference counts. Using it on domain state silently defeats the immutability guarantee.

Best Practices

Mark every non-mutating member function const immediately. If a function only reads *this, it must be const to be callable on const objects and const references. Adding const later cascades into callers.

Use const& for non-trivial function parameters. Apply it whenever the type is larger than two machine words and you do not need a local copy. Since C++11, pass-by-value is often preferable when you intend to own the data, because move semantics make it cheap for movable types.

Prefer constexpr over const for compile-time constants (C++11). constexpr int N = 1024; is guaranteed to be evaluated at compile time and can appear in template arguments and array sizes. const only guarantees the variable is not modified through that name.

cpp
constexpr std::size_t page_size = 4096;  // C++11: compile-time constant
const     std::size_t dynamic  = get();  // runtime value, still immutable

Avoid returning const T by value. const Widget foo() prevents move on the return value in C++11 and later, degrading performance without meaningful safety benefit. Return const T& only when lending access to an internal member.

Use std::as_const instead of manual casts (C++17). When you want a const reference to a non-const object without writing a cast, std::as_const(obj) is self-documenting and less error-prone.

cpp
#include <utility>  // std::as_const — C++17

for (const auto& item : std::as_const(my_map)) { /* read-only traversal */ }

Common Pitfalls

Writing through const_cast. Stripping the const qualifier with const_cast<T&> and then writing through the result is undefined behaviour when the original object was genuinely const. The only legitimate use is interoperating with old APIs that lack const-correct signatures but are known not to mutate.

mutable on semantic state. If a mutable member holds domain data—not a cache or synchronisation primitive—the object's logical value can change through a "const" function, breaking caller assumptions silently.

Unsynchronised mutable state in const member functions. The C++ standard library assumes that calling const member functions concurrently without external synchronisation is safe. If your const function accesses shared mutable state, you must protect it. A data race is undefined behaviour even when the function is const:

cpp
// UB: unsynchronised shared mutable state
class Counter {
public:
    int next() const { return ++count_; } // data race if called concurrently
private:
    mutable int count_ = 0;
};

// Correct: atomic access — C++11
class Counter {
public:
    int next() const {
        return count_.fetch_add(1, std::memory_order_relaxed);
    }
private:
    mutable std::atomic<int> count_{0};  // C++11
};

Omitting const on the out-of-class definition. void Foo::bar() { ... } and void Foo::bar() const { ... } declare two different functions. If the class declaration says const and the definition does not, the compiler will report the non-const version as not declared in the class.

See Also

  • constexpr — compile-time evaluation; introduced in C++11, extended significantly in C++14, C++17, and C++20
  • mutable specifier — per-member opt-out from const restrictions; use sparingly
  • std::as_const — returns a const reference to its argument without a cast (C++17, <utility>)
  • Deducing this — eliminates the need for const/non-const overload pairs (C++23)
  • std::atomic — thread-safe mutation for mutable members in const functions (C++11)