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

mutable

The mutable specifier lets a non-static data member be modified inside const member functions and const objects, enabling logical constness for caches, locks and counters.

mutablesince C++98

A storage-class specifier applicable only to non-static, non-reference, non-const data members that exempts the member from the constness of its enclosing object, and (since C++11) a lambda specifier that makes the closure's operator() non-const.

Overview

const on a member function promises logical constness: the observable state of the object does not change. The compiler, however, enforces bitwise constness: no non-static member may be assigned within a const member function. Many correct designs need to bridge that gap, for example lazy caches, memoised results, lock objects, reference counters and diagnostic statistics. mutable is the language's sanctioned way to say "this member is an implementation detail, not part of the observable value."

Key properties:

  • Applies only to non-static data members of class type (C++98). It cannot be applied to reference members, const members, or static members.
  • A mutable member of a const object may be modified without undefined behaviour. Contrast with const_cast on a genuinely const object, which is UB if you then write to it.
  • Since C++11, mutable has a second meaning in lambda expressions: it removes the implicit const from the closure's call operator so by-copy captures may be modified.
  • mutable does not provide thread safety. A const member function is expected (by the standard library's convention since C++11) to be safe for concurrent calls, so mutable state must be synchronised or atomic.

Syntax

cpp
class Widget {
    mutable int cache_;                 // C++98: data member specifier
    mutable std::mutex m_;              // C++11: common with synchronisation primitives
    static mutable int bad;             // ill-formed: static + mutable
    // mutable const int c;             // ill-formed: const + mutable
    // mutable int& r;                  // ill-formed: reference member
};

// C++11: lambda specifier
auto f = [n = 0]() mutable { return ++n; };

// C++23: mutable may now be combined with an explicit object parameter only
// through the lambda specifier sequence; the rules for the closure are unchanged
auto g = [n = 0](this auto&& self) { return n; };   // C++23, deducing this

In a lambda, the order is [captures](params) mutable constexpr noexcept attr -> ret { body }. constexpr as a lambda specifier is C++17; consteval is C++20. Before C++23, a lambda with no parameter list must write () if mutable is present; since C++23 the parentheses are optional with specifiers.

Examples

Lazy cache in a const accessor

cpp
#include <optional>
#include <string>
#include <vector>
#include <numeric>

class Dataset {
public:
    explicit Dataset(std::vector<double> v) : values_(std::move(v)) {}

    // Logically const: the mean is a pure function of values_
    double mean() const {
        if (!mean_) {                       // std::optional: C++17
            mean_ = std::accumulate(values_.begin(), values_.end(), 0.0)
                    / static_cast<double>(values_.size());
        }
        return *mean_;
    }

    void add(double x) {
        values_.push_back(x);
        mean_.reset();                      // invalidate cache
    }

private:
    std::vector<double> values_;
    mutable std::optional<double> mean_;    // cache; not part of observable state
};

This is not thread-safe: two threads calling mean() concurrently race on mean_.

Thread-safe version with a mutex

cpp
#include <mutex>
#include <optional>

class SafeDataset {
public:
    double mean() const {
        std::lock_guard<std::mutex> lock(m_);   // C++11; CTAD form is C++17
        if (!mean_) {
            mean_ = compute();
        }
        return *mean_;
    }

private:
    double compute() const;                     // reads values_ only
    std::vector<double> values_;
    mutable std::mutex m_;                      // must be mutable to lock in const fn
    mutable std::optional<double> mean_;
};

std::mutex::lock() is non-const, so without mutable the lock could not be taken from a const member function. This is the single most common use of mutable in modern code. Note that making the class thread-safe this way also makes it non-copyable, because std::mutex is neither copyable nor movable. You must write copy and move operations explicitly if needed.

Atomic counter

cpp
#include <atomic>

class Handler {
public:
    void handle() const {
        calls_.fetch_add(1, std::memory_order_relaxed);   // C++11
        // ...
    }
    long calls() const { return calls_.load(std::memory_order_relaxed); }

private:
    mutable std::atomic<long> calls_{0};
};

For a lone counter, std::atomic is cheaper than a mutex. Use a mutex when several members must change together.

Lambda mutable

cpp
#include <algorithm>
#include <vector>

std::vector<int> ids(5);

// C++14: init-capture; C++11: mutable
std::generate(ids.begin(), ids.end(),
              [next = 100]() mutable { return next++; });
// ids == {100, 101, 102, 103, 104}

A lambda's operator() is const by default. Without mutable, next++ is ill-formed. The modification affects the closure's own copy, not the original captured variable, and persists across calls on the same closure object:

cpp
int counter = 0;
auto inc = [counter]() mutable { return ++counter; };
inc(); inc();
// inc() returned 1, then 2; the outer `counter` is still 0

auto copy = inc;      // copies the closure state (currently 2)
copy();               // returns 3; `inc`'s state is unchanged

Note that std::for_each returns a copy of the functor, which is the traditional way to retrieve accumulated state from a stateful function object. Handwritten functors use a mutable member for the same purpose when the operator() must be const.

Best Practices

  • Use mutable only for state that is not part of the object's value. Ask whether two objects that compare equal could differ in that member. If so, it is not a candidate.
  • Pair every mutable with a synchronisation policy. Since C++11, standard-library containers assume const operations are safe to call concurrently. Your types should honour that: use std::mutex, std::atomic, or std::once_flag/std::call_once (C++11) for one-time initialisation.
  • Prefer std::atomic for single-value caches and a mutex when a cache is multi-field.
  • Keep mutable members grouped and commented so readers can see from the declaration which members change in const functions.
  • Prefer mutable to const_cast. const_cast that writes to an object defined const is undefined behaviour; a mutable member is always legal to write.
  • In lambdas, prefer not to use mutable for logic that should be explicit. A stateful closure passed to algorithms can be copied by the algorithm, making results surprising. std::ref or a captured reference is often clearer.

Common Pitfalls

  • Assuming mutable implies thread safety. It only disables the compiler check. Unsynchronised writes from concurrent const calls are a data race (UB since C++11).
  • Cache invalidation bugs. A mutable cache must be invalidated by every non-const mutator. Forgetting one produces stale reads that are hard to reproduce.
  • Using mutable to silence const errors. If a "getter" really mutates observable state, mark it non-const instead. Overuse erodes the value of const as documentation.
  • Copy semantics. Defaulted copy operations copy mutable members. A copied cache is usually fine, but a copied std::mutex is a compile error, and a copied std::atomic is deleted too. Provide custom special members.
  • Lambda copies. Each copy of a mutable lambda has independent state. Passing one by value to std::generate or std::transform then discarding it loses the state, and some algorithms may copy the callable multiple times (no sequence guarantee for std::transform before C++17 parallel overloads clarifications). Do not rely on copy counts.
  • mutable and constexpr. Since C++11 a mutable member in a literal type prevents a constexpr object from being usable in constant expressions that modify it, and a constexpr variable's mutable subobjects cannot be read during constant evaluation (the lvalue-to-rvalue conversion on a mutable member is not permitted in a constant expression unless the object's lifetime began within the evaluation).
  • Const objects in ROM. A const object with a mutable member cannot be placed in read-only memory, since the compiler must assume writes may occur. This matters on embedded targets where it silently moves data into RAM.
  • Mutable members in a const aggregate still prevent some optimisations, because the compiler can no longer assume the whole object is immutable after initialisation.

See Also