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++98A 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,
constmembers, or static members. - A
mutablemember of aconstobject may be modified without undefined behaviour. Contrast withconst_caston a genuinelyconstobject, which is UB if you then write to it. - Since C++11,
mutablehas a second meaning in lambda expressions: it removes the implicitconstfrom the closure's call operator so by-copy captures may be modified. mutabledoes not provide thread safety. Aconstmember 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
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 thisIn 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
#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
#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
#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
#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:
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 unchangedNote 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
mutableonly 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
mutablewith a synchronisation policy. Since C++11, standard-library containers assumeconstoperations are safe to call concurrently. Your types should honour that: usestd::mutex,std::atomic, orstd::once_flag/std::call_once(C++11) for one-time initialisation. - Prefer
std::atomicfor 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
constfunctions. - Prefer
mutabletoconst_cast.const_castthat writes to an object definedconstis undefined behaviour; amutablemember is always legal to write. - In lambdas, prefer not to use
mutablefor logic that should be explicit. A stateful closure passed to algorithms can be copied by the algorithm, making results surprising.std::refor a captured reference is often clearer.
Common Pitfalls
- Assuming
mutableimplies thread safety. It only disables the compiler check. Unsynchronised writes from concurrentconstcalls are a data race (UB since C++11). - Cache invalidation bugs. A
mutablecache must be invalidated by every non-const mutator. Forgetting one produces stale reads that are hard to reproduce. - Using
mutableto silence const errors. If a "getter" really mutates observable state, mark it non-const instead. Overuse erodes the value ofconstas documentation. - Copy semantics. Defaulted copy operations copy
mutablemembers. A copied cache is usually fine, but a copiedstd::mutexis a compile error, and a copiedstd::atomicis deleted too. Provide custom special members. - Lambda copies. Each copy of a
mutablelambda has independent state. Passing one by value tostd::generateorstd::transformthen discarding it loses the state, and some algorithms may copy the callable multiple times (no sequence guarantee forstd::transformbefore C++17 parallel overloads clarifications). Do not rely on copy counts. mutableandconstexpr. Since C++11 amutablemember in a literal type prevents aconstexprobject from being usable in constant expressions that modify it, and aconstexprvariable'smutablesubobjects 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
constobject with amutablemember 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
constaggregate still prevent some optimisations, because the compiler can no longer assume the whole object is immutable after initialisation.