C++ / a working model

15 / 163   ·   C++11   ·   8 min

mutable: Mutable Implementation State in a Logically Read-Only Object

Keep this sentence

mutable allows specific non-static data members to be modified even when the containing object is const, which is appropriate for caches, mutexes, and similar implementation details. It does not automatically provide synchronization, and it should not conceal changes to logical state; mutable on a lambda instead controls whether a by-value capture copy can be modified.

In this lesson
  1. Physical Change Versus Logical Change
  2. A Cache Needs a Complete Invalidation Strategy
  3. The Same Keyword on a Lambda
  4. Example
  5. Exercise

Physical Change Versus Logical Change

A const member function usually cannot modify ordinary data members through this, but some changes should not alter the value the object presents to clients. For example, a query may fill a recomputable cache while the returned result is still determined by the original data; mutable can then mark "implementation state that may be updated during a read-only query".

mutable may be applied only to qualifying non-static data members; it cannot directly decorate a reference member or a top-level const member. It does not strip const from the whole object; it grants a bounded exception to one member. Whether something is logical state is still a matter of interface design; the compiler cannot decide for you whether a cache is reasonable.

A Cache Needs a Complete Invalidation Strategy

The example stores a fixed small integer, computes and marks the cache valid on the first doubled call, and reuses the result on the second query. The object's original value cannot be changed through the public interface, so no extra invalidation branch is needed; if you later add set_value, you must invalidate the cache at the same time.

A const query may also be called concurrently by several threads. Two threads updating a mutable cache with no synchronization still have a data race. You can protect the cache inside the query with a mutable mutex, precompute, or use another proven synchronization scheme; mutable itself is neither a lock nor an atomic operation.

The Same Keyword on a Lambda

For a lambda that captures by value, mutable makes the call operator no longer const by default, so you can modify the captured copies stored in the closure object. It does not change the capture into a reference, and it does not make the original outer variable follow the copy. The example increments the copy and then checks that the outer variable still has its original value.

When capturing by reference, whether the target can be modified depends on the target object's own qualifiers and the way it is accessed; you do not need mutable just to write a non-const object that was captured by reference. Copying a mutable closure also copies its state, so the two closures may then evolve independently; when returning a closure, distinguish independent state from a shared reference with particular care.

Pitfalls

  • Marking every member mutable hollows out the constraints of a const interface; mark only implementation details that truly do not change the logical value.
  • Missed cache invalidation and thread races are two independent problems; fixing one does not mean the other is solved.

Run an example

Minimum C++11 · complete program · Download .cpp

#include <cassert>
#include <iostream>

class Sample {
    const int value_ = 3;
    mutable bool ready_ = false;
    mutable int cached_ = 0;
public:
    int doubled() const {
        if (!ready_) {
            cached_ = value_ * 2;
            ready_ = true;
        }
        return cached_;
    }
};

int main() {
    const Sample sample;
    assert(sample.doubled() == 6);
    assert(sample.doubled() == 6);
    int original = 4;
    auto next = [original]() mutable { return ++original; };
    const int captured_value = next();
    assert(captured_value == 5 && original == 4);
    std::cout << sample.doubled() << ' ' << captured_value << ' '
              << original << '\n';
}

Compile locally

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread basics-mutable.cpp -o example && ./example

Expected result

6 5 4

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

auto f = [n]() mutable { return ++n; }; auto g = f;, with the outer n initially 0. What are the results of calling f(), f(), and g() in that order?

Show a reference answer

The successive results are 1, 2, and 1, and the outer n remains 0. f first holds a capture copy whose value is zero; g copies that closure state before any call occurs; the two internal copies are independent. mutable allows each copy to be modified and does not make them share one counter. If shared state is required, design object ownership and any needed concurrent synchronization explicitly.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog