iToverDose/Software· 28 MAY 2026 · 08:02

How to Encapsulate Internal State in Modern C++ Cleanly

Learn how to keep your domain model clean by isolating module-internal state in C++ using pure functions and namespaces, avoiding pollution and unintended dependencies.

DEV Community4 min read0 Comments

Modern C++ applications often struggle with state management when internal implementation details leak into the domain model. When system components require temporary storage to handle asynchronous operations—like a game's command queue—traditional approaches risk cluttering core logic with extraneous state. The solution lies in encapsulating these internal details without resorting to mutable object-oriented patterns.

Why Internal State Needs Special Handling

Domain state represents the core concepts of an application: players, game boards, or financial transactions. However, not all state belongs to this model. Consider a parser tracking its position in an input stream or a cache storing recent lookups—these are internal implementation details. Treating them as domain state pollutes the model, making it harder to reason about and increasing the risk of unintended dependencies.

In object-oriented C++, encapsulation is typically achieved through class interfaces that hide mutable state. But when aiming to maintain functional programming principles—such as pure functions and immutable data—this approach falls short. The challenge becomes: How can we isolate internal state while preserving explicit state evolution?

Introducing Stateful Functional Modules

A stateful functional module addresses this by grouping a state definition with pure functions that define how that state evolves. The internal state module variant focuses specifically on state that exists solely to support a module’s internal logic. Unlike domain state, this state isn’t shared across the system but remains confined to the module where it belongs.

The shell—typically the outermost layer of the application—persists and updates this state by replacing its current value with the result of a pure function call. The key difference lies in how the state is treated: it’s threaded through the system as module-local implementation state, not shared domain state. This creates a clear encapsulation boundary where the shell interacts with the state based on its type alone, without understanding its internal structure.

A Practical Example: Direction Command Filtering

Let’s examine a real-world implementation from the funkysnakes project. In this game, snakes are controlled via arrow keys, but key events and game loops operate asynchronously. At the start of each game tick, the snake’s movement direction is updated based on newly received key events. This logic is handled by the direction_command_filter module, which filters key-press events to determine valid direction changes.

The module requires internal state: a queue of direction commands per player. This state exists purely to implement the filtering logic and isn’t part of the game’s domain state (e.g., snakes, food, or the board). Here’s how it’s defined:

namespace direction_command_filter {
    struct State {
        using PerPlayerDirectionQueue = std::map<PlayerId, std::deque<Direction>>;
        PerPlayerDirectionQueue queues;
    };
}

The module exposes two functions: tryAdd, which inserts new direction commands, and tryConsumeNext, which retrieves the next valid direction. Both functions operate on the module’s state and return an updated state:

namespace direction_command_filter {
    State tryAdd(State state, const PerPlayerSnakes& snakes, const DirectionCommand& cmd);
    std::tuple<State, PerPlayerDirection> tryConsumeNext(State state);
}

The domain state (PerPlayerSnakes) and module state (direction_command_filter::State) coexist within the shell’s GameState:

namespace shell {
    struct GameState {
        ...
        PerPlayerSnakes snakes;
        direction_command_filter::State direction_command_filter_state;
    };
    
    class GameEngineActor : public Actor<GameEngineActor> {
        ...
        GameState game_state_;
    };
}

The GameEngineActor threads the module state through these functions:

state_.direction_command_filter_state = direction_command_filter::tryAdd(
    state_.direction_command_filter_state,
    state_.snakes,
    new_command
);

auto [new_state, direction] = direction_command_filter::tryConsumeNext(
    state_.direction_command_filter_state
);
state_.direction_command_filter_state = new_state;

The Underlying Pattern

The approach follows a consistent pattern across modules. A module defines a State type and a set of pure operations over that state. Each operation receives the current state and input, returning the updated state:

namespace module {
    struct State { ... };
    State operation(State state, Input input);
}

The namespace serves as the module boundary, grouping the state and operations that define its evolution. This design allows the module to be tested independently by constructing its state, calling its functions, and verifying the returned state. The namespace also provides a clear scope for operations, similar to a class name in object-oriented programming. Client code interacts with module::operation and stores module::State, ensuring the module boundary remains visible at the call site.

From the shell’s perspective, the interaction is straightforward:

namespace shell {
    module::State module_state;
    module_state = module::operation(module_state, input);
}

Here, the shell persists the state between calls and threads it through the module’s operations, treating it as module-local implementation state rather than shared domain knowledge.

The Benefits of This Approach

By isolating internal state within modules, developers gain several advantages:

  • Cleaner domain models: Internal implementation details no longer clutter the core logic.
  • Reduced coupling: Modules depend only on their own state and inputs, not external state structures.
  • Easier testing: Modules can be unit-tested in isolation by controlling their state and inputs.
  • Explicit state evolution: State changes remain transparent, as they’re managed through pure functions.
  • Scalability: New modules can be added without risking pollution of existing domain state.

This pattern bridges the gap between functional programming principles and practical C++ development, offering a clean way to manage state without sacrificing encapsulation or clarity.

As applications grow in complexity, the need for disciplined state management becomes more critical. By adopting stateful functional modules, teams can build systems that are both robust and maintainable, where internal details remain exactly where they belong—encapsulated and isolated from the domain.

AI summary

C++ projelerinizde durum yönetimini nasıl daha anlaşılır ve bakımı kolay hale getirebilirsiniz? Fonksiyonel programlama prensiplerini kullanarak domain modelinizi korurken iç durumları nasıl kapsayacağınızı öğrenin.

Comments

00
LEAVE A COMMENT
ID #41T98B

0 / 1200 CHARACTERS

Human check

3 + 7 = ?

Will appear after editor review

Moderation · Spam protection active

No approved comments yet. Be first.