C++ • SOFTWARE CRAFT AND COMMUNICATION

Minimizing Global State & Const Correctness — Minimize global state and use const correctness

Taming mutable shared state and enforcing immutability contracts to build robust, maintainable C++ software.

Historical Context & Motivation

The tension between convenient global state and disciplined locality of data has shaped the evolution of programming languages since the earliest days of computing. In the 1950s and 1960s, programs written in assembly and early FORTRAN routinely communicated through shared memory locations — what we now call global variables. As programs grew from hundreds to millions of lines, the hidden dependencies introduced by globals became a primary source of defects, making reasoning about program behavior exceedingly difficult. The quest to constrain mutability led to the introduction of const correctness in C++ — a compile-time mechanism that encodes immutability directly into the type system.

1972
C Language & Global by Default
Dennis Ritchie's C language uses file-scope variables extensively. Without namespaces or access control, any translation unit can reach any external symbol, making large-scale programs fragile.
1979
Bjarne Stroustrup Begins 'C with Classes'
Stroustrup introduces classes to C, enabling encapsulation. Data can now be hidden behind private interfaces, reducing reliance on global state. The idea of controlling access to data at compile time takes root.
1985
C++ 1.0 and the const Keyword
The first commercial release of C++ includes the const qualifier, allowing programmers to declare that a variable or parameter should not be modified. This is a significant departure from C's weaker #define constants.
2011
C++11: constexpr and Move Semantics
The C++11 standard extends const correctness with constexpr, enabling compile-time evaluation of functions and variables. Move semantics further reduce the need for shared mutable state by enabling efficient value transfers.
2020
C++20: consteval, constinit, and Modules
C++20 introduces consteval (immediate functions) and constinit (guaranteeing constant initialization of globals), directly addressing the static initialization order fiasco that plagues global state.

Despite decades of language evolution, global mutable state persists in production codebases. The fundamental question this lesson addresses is: how can a C++ programmer systematically minimize global state and leverage the type system's const qualifiers to communicate and enforce immutability contracts? Answering this question is central to writing software that is testable, thread-safe, and maintainable.

Core Principles & Definitions

Before diving into implementation strategies, it is essential to establish the foundational ideas that underpin both minimizing global state and applying const correctness. These principles are not merely stylistic preferences; they arise from the formal desire to restrict the set of states a program can occupy at any point in its execution, thereby simplifying reasoning, testing, and concurrent execution.

1

Principle of Least Authority (POLA)

Every component should receive only the data and permissions it needs — no more. A function that only reads a vector should receive it by const&, never by non-const reference or pointer to a global.
2

Referential Transparency

A function exhibits referential transparency when its return value depends solely on its arguments. Global mutable state breaks this property because the same call can yield different results depending on hidden program state.
3

Const as a Contract

Marking a parameter, member function, or variable as const is a promise enforced by the compiler: this entity will not be mutated through this path. It documents intent and catches violations at compile time.
4

Static Initialization Order Fiasco

Global objects in different translation units have an unspecified initialization order. If one global depends on another, your program may exhibit undefined behavior at startup — a notorious C++ pitfall.
5

Thread Safety by Design

Shared mutable state requires synchronization (mutexes, atomics). Const data and local state are inherently thread-safe, eliminating entire categories of concurrency bugs without runtime overhead.
✦ KEY TAKEAWAY
Think of global mutable state like a shared whiteboard in a busy office. Anyone can erase and rewrite it at any time, so you can never be sure the information you read is the information you expect. Const correctness is like laminating the whiteboard — people can still read it, but nobody can alter it without going through an explicit, auditable process. Minimizing global state means replacing that shared whiteboard with individual notepads handed directly to the people who need them.

Visual Explanation — Data Flow Architectures

The following diagram contrasts two architectural patterns: a program that routes data through global variables versus one that passes data explicitly through function parameters. The left side shows the tangled dependency web created by global state, while the right side demonstrates clean, explicit data flow with const-qualified interfaces.

Left: global variables g_config, g_db, and g_logger create hidden coupling between all functions. Right: main() owns the data and passes it explicitly — read-only access is enforced via const& parameters.

In the left-hand architecture, every function implicitly depends on every global variable, creating an N × M coupling surface. When you modify g_config, you must audit every function in the program to understand the impact. In the right-hand architecture, the function signature itself documents what data flows in, and whether it can be modified. This explicitness is the cornerstone of maintainable C++ design. Notice that validate takes a const Config& — the compiler will reject any attempt to mutate the configuration inside that function, providing a machine-checked guarantee rather than a comment or convention.

How Const Correctness Works in the Type System

C++ treats const as a type qualifier — int and const int are distinct types. This distinction propagates through the type system: a const int* (pointer to const int) cannot be implicitly converted to an int*. The compiler enforces these rules at every assignment, function call, and member access, creating a web of compile-time checks that prevent accidental mutation.

The Four Faces of Const

The four common const declarations in C++
DeclarationWhat Is Const?Example
const int x = 42;The variable itself — cannot be reassignedx = 10; // ERROR
const int* p;The pointed-to data — pointer can be reseated*p = 10; // ERROR, p = &y; // OK
int* const p = &x;The pointer itself — cannot be reseated*p = 10; // OK, p = &y; // ERROR
void foo() const;Member function — promises not to modify the object's observable stateCan be called on const objects and const references

Const Propagation Rules

When a member function is declared const, the implicit this pointer becomes const T*. This means every data member accessed through this is treated as const, and only other const member functions may be called on the object. This is known as shallow const propagation — if a class holds a raw pointer member, the pointee is not automatically const. For deep const semantics, the programmer must use std::experimental::propagate_const or design wrapper types that propagate constness to owned resources.

⚠️ The mutable Escape Hatch
The mutable keyword allows a data member to be modified even within a const member function. This is legitimate for caches, mutexes, and lazy-evaluation bookkeeping — it preserves logical constness while permitting internal optimization. Use it sparingly, and document the invariant it protects.

Strategies for Minimizing Global State

Eliminating global state entirely is sometimes impractical — logging infrastructure, hardware register maps, and certain singleton services may demand program-wide accessibility. The goal is not zero globals but minimal, controlled, and well-encapsulated global state. The following strategies, listed from most aggressive to most pragmatic, provide a toolkit for achieving this goal.

Five strategies for replacing or controlling global state, ordered from most preferred (dependency injection) to last resort (file-scope mutable global). Each successive strategy trades explicitness for convenience.

Strategy 1 — dependency injection — deserves particular emphasis because it simultaneously eliminates global state and naturally invites const correctness. When a class receives its dependencies through a constructor taking const ILogger& or const Config&, the compiler enforces that the class cannot modify those resources. Furthermore, unit tests can inject mock objects without manipulating any global state, dramatically improving testability.

Strategy 3 — namespace-scoped constants — is worth highlighting because constexpr constants are not global state at all; they are compile-time values baked into the binary. A constexpr double pi = 3.14159265358979; at namespace scope poses no threading risk, no initialization-order risk, and no testability problem. The distinction between global data (safe if const) and global mutable state (dangerous) is crucial.

Worked Example — Refactoring a Global-Heavy Module

Consider a small module that processes sensor readings. The original version uses two global variables and lacks const correctness. We will refactor it step by step, demonstrating how to apply the principles discussed earlier.

Refactoring Sensor Processing Module
1
Step 1 — Identify Global StateThe original code declares std::vector<double> g_readings; and double g_threshold = 5.0; at file scope. Three functions — addReading(double), filterReadings(), and averageFiltered() — all read and write these globals implicitly. Our audit reveals that g_threshold is never modified after startup, while g_readings is mutated by addReading and filterReadings.
Identified: 2 globals, 3 functions with hidden dependencies
2
Step 2 — Make the Immutable Global constexprSince g_threshold never changes, we replace it with constexpr double kThreshold = 5.0; inside an appropriate namespace. The 'k' prefix follows the Google C++ Style Guide convention for constants. This value is now a compile-time constant: it has no address at runtime (unless ODR-used), poses no initialization-order risk, and the compiler may inline it at every use site.
namespace sensor { constexpr double kThreshold = 5.0; }
3
Step 3 — Convert the Mutable Global to a Local with Explicit PassingWe move the std::vector<double> into main() and pass it to functions that need it. addReading takes a non-const reference because it must mutate the vector: void addReading(std::vector<double>& readings, double value). The function signature now advertises that it modifies the vector.
void addReading(std::vector<double>& readings, double value);
4
Step 4 — Apply Const Correctness to Read-Only ConsumersaverageFiltered only reads the vector, so its signature becomes double averageFiltered(const std::vector<double>& readings). If we later accidentally add a push_back call inside this function, the compiler will reject it. The filterReadings function returns a new filtered vector rather than mutating the original, allowing us to pass the original by const reference: std::vector<double> filterReadings(const std::vector<double>& readings, double threshold).
double averageFiltered(const std::vector<double>& readings);
5
Step 5 — Verify in main()The final main() function creates a local std::vector<double> readings;, populates it via addReading(readings, val), computes auto filtered = filterReadings(readings, sensor::kThreshold), and obtains const double avg = averageFiltered(filtered). There are now zero global mutable variables. Every function's dependencies are visible in its signature. The code is trivially unit-testable: pass a known vector, assert the result.
Zero global mutable state. All dependencies explicit. Fully unit-testable.

Trade-offs — Benefits and Costs

Like most engineering practices, minimizing global state and applying const correctness involve trade-offs. While the benefits overwhelmingly favor disciplined use in production systems, it is important to understand the costs honestly to make informed design decisions.

Benefits and costs of minimizing global state and enforcing const correctness
AspectBenefitCost / Limitation
ReadabilityFunction signatures document exactly what data flows in and whether it is mutable — self-documenting code.Functions may develop longer parameter lists when dependencies are injected explicitly instead of accessed globally.
TestabilityPure functions with no global dependencies can be tested in isolation with deterministic inputs and outputs.Requires designing interfaces (abstract classes / templates) for dependency injection, adding some upfront design effort.
Thread SafetyConst data is inherently safe for concurrent reads. Eliminating shared mutable state removes data races by construction.Shallow const on pointer members does not guarantee deep immutability; extra wrapper types may be needed.
Performanceconstexpr enables compile-time computation. Optimizer can reason more aggressively about const references.Passing objects through deep call stacks adds pointer indirections; in practice, the cost is negligible on modern hardware.
Codebase MigrationGradual adoption is possible — start by adding const to leaf functions and working upward through the call graph.Retrofitting const into a large legacy codebase can trigger cascading changes ('const poisoning'), requiring substantial refactoring.
✦ KEY TAKEAWAY
The phenomenon sometimes called 'const poisoning' — where adding const to one function forces changes throughout the call graph — is actually the type system revealing hidden mutation contracts that were always there. It is better to surface these contracts at compile time than to discover them as runtime bugs in production. Think of it like a structural engineer X-raying a building: the cracks were already there — the X-ray just made them visible.

Connection to Advanced Techniques

The principles of minimizing global state and const correctness are foundational stepping stones toward more advanced C++ and software engineering concepts. Understanding these connections helps situate today's lesson within the broader trajectory of a computer science education.

Mapping today's concepts to advanced C++ and software engineering topics
This LessonAdvanced Extension
const qualifier on variables and parametersconstexpr and consteval for compile-time computation; template metaprogramming operates on types (which are inherently immutable)
Passing dependencies explicitly via parametersFormal dependency injection frameworks (e.g., Boost.DI); the Dependency Inversion Principle (the 'D' in SOLID)
Shallow vs. deep conststd::experimental::propagate_const<T>; Rust's ownership model enforces deep immutability by default with &T references
Eliminating shared mutable state for thread safetyLock-free programming; std::shared_mutex with reader/writer semantics; actor model (message passing replaces shared state)
Meyer's Singleton (function-local static)Service locator pattern; compile-time dependency resolution via policy-based design (Alexandrescu's Modern C++ Design)

As you progress to courses in concurrent systems, compiler construction, or formal verification, you will see these ideas recur in increasingly powerful forms. Languages like Rust have made immutability the default — variables are immutable unless explicitly declared mut. Functional programming languages like Haskell enforce referential transparency at the language level. In C++, these guarantees are opt-in via const, which places the responsibility squarely on the programmer — making the discipline of applying const by default and removing it only when mutation is genuinely needed all the more important.

Practice Problems

PROBLEM 1 — CONCEPTUAL
Explain why a constexpr variable at namespace scope is fundamentally different from a mutable global variable in terms of thread safety and initialization ordering. Under what circumstances, if any, could a constexpr global still pose a design problem?
PROBLEM 2 — BASIC CALCULATION
Given the function signature void process(std::vector<int>& data, const Config& cfg, Logger* log), identify which parameters can be mutated by process and which cannot. Then rewrite the signature to also make the Logger a non-owning, read-only dependency, assuming logging has const-correct methods.
PROBLEM 3 — INTERMEDIATE
A colleague writes a class Cache with a method std::string lookup(const std::string& key) that internally updates a std::unordered_map<std::string, std::string> to store recently accessed entries. They want to call lookup on a const Cache&, but the compiler rejects it. Explain why, and propose two different solutions.
PROBLEM 4 — APPLIED
You are writing a multi-threaded server. A ConnectionPool object is created in main() and shared among request-handler threads. Currently it is a global variable. Refactor the design so that: (a) the ConnectionPool is no longer global, (b) handler threads receive a reference to it, and (c) you maximize const correctness. Sketch the key type signatures and explain your thread-safety strategy.
PROBLEM 5 — CRITICAL THINKING
A purist colleague argues that all globals should be eliminated, including Meyer's singletons. Another colleague argues that Meyer's singleton is fine because function-local statics have well-defined initialization semantics in C++11. Evaluate both positions. Under what conditions would you accept a Meyer's singleton over full dependency injection, and how would you apply const correctness to mitigate the remaining risks?

Lesson Summary

Global mutable state creates hidden dependencies, breaks referential transparency, introduces thread-safety hazards, and triggers the static initialization order fiasco. Minimizing global state involves preferring dependency injection and local objects with explicit lifetime over file-scope variables, while reserving namespace-scoped constexpr constants for truly immutable values and Meyer's singleton for the rare cases where program-wide infrastructure demands a single instance.

Const correctness leverages C++'s type system to enforce immutability contracts at compile time. The four key applications are: const variables, pointer-to-const vs. const-pointer, const member functions (which make this a const pointer), and const reference parameters that document read-only intent. The mutable keyword provides a disciplined escape hatch for caches and synchronization primitives. Together, these practices produce code that is self-documenting, testable, and inherently safer in concurrent environments.

Varsity Tutors • C++ • Minimizing Global State & Const Correctness