Overview

5 Purity: Avoiding mutable state

This chapter argues that much of software complexity and many bugs stem from mutable state shared across components. It introduces purity and the related concept of referential transparency as the foundation for reasoning about programs: expressions should be replaceable by their values without altering observable behavior. While real programs must interact with the world, the goal is to minimize mutations and isolate side effects, structuring systems so a pure, deterministic core operates over immutable data and a thin boundary handles I/O and other effects.

Through examples, the text shows how shared mutation undermines correctness and composability. A seemingly straightforward average computation over a movie’s scores can yield inconsistent results if the list changes mid-calculation or if redundant, interdependent fields (like a cached size) drift out of sync. Making data immutable removes these hazards. Purity is clarified via referential transparency: a max function that logs to an error stream is not pure because replacing the call with its value changes program behavior. The chapter demonstrates how to “program without side effects” by returning new values rather than changing existing ones (e.g., computing a player’s next position in a maze), and recommends separating a pure state-transition core from the presentation and event loop. For larger systems, pure functions can return “what to change” descriptions that an effectful layer applies, keeping the boundary between pure and impure code explicit.

Concurrency magnifies the risks of shared mutation: even decrementing a shared counter can race, and mutexes—while sometimes necessary—serialize execution and cap scalability (per Amdahl’s law). Safer choices are to avoid sharing, use immutable data, or confine mutability to single owners. C++ provides tools to enforce these designs: const and constexpr restrict mutation; const-correctness for references, pointers, and member functions treats the implicit this as const to prevent unintended changes; logical and internal constness can coexist, with carefully controlled mutable members enabling thread-safe caching. The chapter also covers performance-aware immutability (ref-qualified “with_” methods that move from temporaries) and cautions about pitfalls: const can inhibit moves when return-value optimizations don’t apply, and “shallow const” on pointer members leaves pointees mutable unless disciplined (e.g., via pointer wrappers). The overarching guidance is to favor const pervasively, isolate side effects, and introduce controlled mutability only where it is demonstrably beneficial.

The result of the calculation depends on the order of evaluation of the division operands and on the place where the new item is inserted while you’re calculating the average score.
c05_01.png
The maze is defined by a matrix in which each cell can be a wall or a hallway. The player can move around but can’t go through the walls.
c05_02.png
Two concurrent processes want to decrement a value. Each needs to retrieve the value from memory, decrement that value, and store it back into memory. Because the initial value is 2, you expect the result after decrementing it twice to be 0. But if the second process is allowed to read the value from memory before the first process has stored its result back into memory, you may end up with a wrong result.
c05_03.png
It would be ideal to have a tenfold speedup when increasing the number of processors 10 times. Unfortunately, the speedup doesn’t increase linearly with the number of processors. According to Amdahl’s law, if you have only 5% of your code serialized (through mutexes or other means), the most speedup you can achieve compared to running the code on a single processor is 20—even if you throw in 60,000 processors.
c05_04.png
You can have both mutable and immutable states; you can share data or not. The only option that produces problems in the concurrent environment is when the data is both shared and mutable. The other three options are safe.
c05_05.png

Summary

  • Most computers have multiple processing units. When writing programs, you need to ensure that your code works correctly in multithreaded environments.
  • If you overuse mutexes, you’ll limit the level of parallelism your program can achieve. Because of the need for synchronization, program speed doesn’t grow linearly with the number of processors you’re using.
  • Mutable state isn’t necessarily bad; having it is safe as long as it isn’t shared between multiple system components at the same time.
  • When you make a member function const, you promise that the function won’t change any data in the class (not a bit of the object will change), or that any changes to the object (to members declared as mutable) will be atomic as far as the users of the object are concerned.
  • Copying the entire structure when you want to change only a single value isn’t efficient. You can use special immutable data structures to remedy this (chapter 8 covers immutable data structures).

FAQ

What problems does mutable state introduce, even when data is “encapsulated” in classes?Mutable state creates hidden coupling and timing-dependent bugs. Examples include: - Races during iteration: computing an average while another thread inserts into the list can yield inconsistent sums and sizes (order of evaluation is unspecified). - Invariants across fields: duplicating data like scores and scores_size requires every mutating path to keep them in sync; a missed update leads to subtle, hard-to-diagnose errors.
What is referential transparency and how does it relate to pure functions?Referential transparency means you can replace an expression with its value without changing program behavior. A function used in such an expression is pure: it depends only on its inputs, has no observable side effects, and returns the same result for the same inputs. Logging (for example to std::cerr) breaks referential transparency because removing the call changes program behavior.
Can I be “pure” and still do I/O? How should I structure such programs?Yes—by isolating side effects at the boundaries. Typical pattern: - Core: pure functions that transform input values to output values. - Shell: minimal impure code that performs I/O (read input, display output, commit changes), calls the pure core, and applies results.
How do I model updates without mutating objects?Return new values instead of changing existing ones. For example, compute a player’s next position in a maze from (direction, previous_position, maze) and return the new position, leaving the old one untouched. This eliminates in-place mutation and race-prone shared updates.
When is mutable state acceptable or even useful?When it localizes change and improves practicality: - UI/event loops where a small part (e.g., current position) changes frequently. - Performance-sensitive caches hidden behind const interfaces. - Systems where copying entire “world” objects would be prohibitively expensive. Keep a clear boundary: pure domain logic plus a thin mutable/side-effecting layer.
Why is shared mutable state dangerous in concurrent code, and what are the alternatives?Because read-modify-write steps interleave unpredictably, producing data races (e.g., decrementing a shared counter twice but ending at 1). Options: - Use mutexes sparingly (they serialize code and reduce parallelism). - Prefer: no sharing; or share immutable data; or use thread-local mutable data and exchange messages/commands. Of the four quadrants (mutable/immutable × shared/not-shared), only “shared + mutable” is hazardous.
What does const actually enforce in C++ (variables, pointers, member functions)?Highlights: - Variables: const T prevents modifying the object through that name. - References/pointers: you can bind const T&& or const T& safely; you can point to const T but not to non-const through a const source. - Member functions: the const qualifier makes the implicit this a pointer to const, disallowing mutation of data reachable through this (directly). It supports both “logical” and “internal” constness when designed carefully.
How can I implement caching while keeping a const interface?Use mutable members for the cache and synchronization: - A mutable cache object and a mutable mutex. - In a const member function, lock, populate on first use, then return cached data. - Ensure concurrent calls don’t observe inconsistent results (synchronize loads) so the class remains logically immutable to callers.
What are common pitfalls when overusing const?Two big ones: - Moving disabled: returning a const object can prevent move optimizations when NRVO doesn’t apply, causing extra copies. - Shallow const: const on a container of pointers forbids changing the pointers but not the pointed-to objects; you can inadvertently mutate through those pointers. Consider propagate_const (where available) or smart-pointer types that enforce const propagation.
How do I avoid extra copies in immutable “setter” methods like with_name(...)?Provide ref-qualified overloads: - const & overload: works on lvalues; returns a modified copy. - && overload: works on temporaries; move-from the source to build the result, avoiding an extra copy. This preserves the immutable API while keeping chains of temporary transformations efficient.

pro $24.99 per month

  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose one free eBook per month to keep
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime

lite $19.99 per month

  • access to all Manning books, including MEAPs!

team

5, 10 or 20 seats+ for your team - learn more


choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Functional Programming in C++ ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Functional Programming in C++ ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Functional Programming in C++ ebook for free