Hello! In our previous lessons, we've explored several powerful concurrency and architectural patterns, such as the Monitor Object, Thread Pool, and Active Object, mostly in isolation. In our last session, we built a simple event loop, which forms the core engine for a worker thread.
Today, we'll tie everything together. This lesson is about synthesis—seeing how individual patterns are not used in a vacuum but are composed to solve larger, more complex architectural problems. Mastering this skill is a significant step from being a programmer who uses features to an engineer who designs systems, a key capability for the roles you're targeting.
Our goal is to analyze the composition of multiple patterns to solve a larger architectural problem, focusing on examples like combining the Command and Active Object patterns. We'll deconstruct the Active Object to see the patterns within it and then see how it serves as a building block in a much larger system.
The Active Object: A Pattern of Patterns
Let's revisit the Active Object pattern. Its primary goal is to decouple method invocation from method execution, allowing a client to make a non-blocking call while the work is performed asynchronously by a dedicated thread.

This pattern is a powerful abstraction, but it's not monolithic. It's actually a beautiful composition of several other, smaller patterns:
- Proxy: The public interface the client interacts with is a Proxy. It has the same method signatures as the real object but doesn't perform the work. Its responsibility is to package the method call and its arguments into a message.
- Command: The packaged message is a Command object. It encapsulates a request as an object. In modern C++, this is often implemented with a type-erased callable like
std::function, which can wrap a lambda containing the method to call and its captured arguments. - Blocking Queue (Monitor Object): A thread-safe queue holds the Command objects. This queue acts as the communication channel between the client thread (the producer) and the Active Object's worker thread (the consumer). This component is a classic example of the Monitor Object pattern. The event loop we built in the last lesson implemented the consumer side of this.
- Scheduler: A dedicated worker thread that continuously pulls Command objects from the queue and executes them. This is the "active" part of the pattern.
- Servant: The underlying object that contains the actual business logic. The Scheduler invokes methods on the Servant.
- Future (Optional): If a method call needs to return a value, the Proxy returns a
std::future. The Scheduler, upon completing the task, uses astd::promiseto set the result, which fulfills the future.
Case Study 1: Composing Command and Active Object with Type Erasure
The central challenge in implementing the Active Object pattern is handling the queue: how do you store different types of tasks (e.g., processInput(InputData) and processHangup(HangupData)) in the same queue?
The typical C++ solution is to use std::function<void()>. This works through a powerful technique called Type Erasure. You're not just using a library feature; you're leveraging a sophisticated composition of patterns under the hood.
To understand this composition, let's watch a segment from an excellent talk by Klaus Iglberger.
Design Patterns: Facts and Misconceptions - Klaus Iglberger - CppCon 2021
This video, 'Design Patterns: Facts and Misconceptions,' dives deep into the intent and structure of patterns. The following section masterfully deconstructs std::function into its constituent patterns, revealing it as a prime example of composition.
Please watch the segment from 43:12 to 47:39. Focus on how std::function uses a combination of three patterns—External Polymorphism, Bridge, and Prototype—to achieve type erasure.
As the video explains, std::function isn't magic. It's a combination of:
- External Polymorphism: It imposes a polymorphic interface (the call operator
()) on types that were not designed with a common base class. It does this by creating its own internal, private class hierarchy. - Bridge: It decouples the
std::functioninterface from the implementation (the stored callable) using a pointer to an abstract base class (the "pimpl" idiom is a simple form of this). - Prototype: It uses a virtual
clonefunction to handle copying, allowing it to duplicate the wrapped callable object without knowing its concrete type.
This combination of patterns to solve the problem of type erasure is a perfect example of what it means to analyze pattern composition.
Now, let's see how we can apply this knowledge to build a more efficient Active Object. While std::function is convenient, its potential for heap allocation can be a performance bottleneck in high-throughput systems.
Active Object, External Polymorphism and Coroutines
The article 'Active Object, External Polymorphism and Coroutines' by Ivan Kostruba provides a practical, step-by-step guide to building an Active Object and demonstrates this exact composition. It starts by using std::function and then replaces it with a custom, high-performance type-erased wrapper.
Read the sections 'Active Object Internals' and 'External Polymorphism Implementation'. Pay close attention to how the TaskWrapper class is implemented. It manually creates a vtable of function pointers (run, destroy, clone) and uses a small internal buffer (buf_) to store the callable object, avoiding heap allocation for small callables (Small Buffer Optimization). This is a from-scratch implementation of the concepts discussed in the video.
The TaskWrapper is a fantastic example for understanding how things work from first principles. It shows you that there's nothing magical about std::function—it's just a well-engineered application of fundamental design patterns, which you can replicate and customize yourself. The benchmark in the article (LINK, section 4) confirms the performance advantage of this approach by avoiding heap allocations.
Test your understanding!
In the custom TaskWrapper from the article, what is the purpose of the vtable_for template variable? How does it help "erase" the type of the Callable object?
Show answer
The vtable_for<Callable> template is instantiated once for each unique type of callable object (Callable) you store in a TaskWrapper.
- Purpose: It creates a static, constant
vtablestruct containing function pointers (run,destroy,clone, etc.). These function pointers point to lambdas that do know the concrete typeCallable. - How it erases the type: When a
TaskWrapperis constructed with a specific callable, it stores two things:- A pointer to the corresponding
vtable_for<Callable>::vtable. - The callable object itself, type-punned into a raw
void*or acharbuffer (buf_).
- A pointer to the corresponding
Later, when you call operator() on the TaskWrapper, it doesn't know the original type. It just follows its vtable_ pointer and calls the run function, passing the raw buffer pointer. The run function (the lambda inside vtable_for) then safely static_casts the void* back to the correct Callable* and invokes its operator(). In this way, the type information is "erased" from the TaskWrapper itself but preserved within the specific vtable it points to.
Case Study 2: Architectural Composition with Half-Sync/Half-Async
We've seen how a single pattern, the Active Object, is composed of others. Now let's zoom out and see how the Active Object itself becomes a building block in a larger, system-level architecture. A classic example is the Half-Sync/Half-Async pattern.
Concurrent Object-Oriented Network Programming with ...
We'll use the 'Concurrent Object-Oriented Network Programming' slides by Douglas C. Schmidt. This is a foundational resource that details the architecture of high-performance concurrent systems. We will focus on how it combines patterns to build a flexible and efficient application gateway.
First, skim the 'Application-level Gateway Example' (starting on slide 115) to understand the problem. Notice the diagram on slide 119, which lists the many patterns being composed. Then, focus deeply on the 'Half-Sync/Half-Async Pattern' section (slides 177-180). Study its intent, structure, and collaboration diagrams. This is the key architectural pattern we are analyzing.
The Half-Sync/Half-Async pattern is a brilliant solution for a common problem in concurrent servers: how do you get the high performance of asynchronous I/O without making all your business logic incredibly complex?
The pattern splits the system into three layers:
-
Asynchronous (or "Async") Layer: This layer interacts with external event sources. It's typically implemented with the Reactor pattern, which uses an event demultiplexer (like
select,poll, orepollon Linux) to handle many I/O sources in a single thread without blocking. Its job is to receive data efficiently and pass it on. -
Queueing Layer: This is the communication channel between the async and sync layers. It's a set of thread-safe message queues.
-
Synchronous (or "Sync") Layer: This layer consists of multiple threads that perform the actual business logic. Each thread pulls a message from a queue and processes it synchronously (i.e., using simple, blocking code). This layer is often implemented as a pool of worker threads or, more elegantly, as a set of Active Objects.
Composing the Gateway
Let's see how these patterns compose in the gateway example from the slides:
- The Reactor (async layer) waits for data to arrive on any of the connected peer sockets.
- When data arrives for an
InputChannel, the Reactor dispatches the event. TheInputChannel'shandle_input()method is called. This happens in the Reactor's thread. - The
InputChannelreads the data, consults a Router to determine the destination, and thenputs the message into the message queue of the appropriateOutputChannel. Thisputoperation is non-blocking and returns immediately. - The
OutputChannelis an Active Object (part of the sync layer). Its dedicated worker thread is sleeping, waiting for a message to appear in its queue. - The
putoperation wakes theOutputChannel's worker thread. The thread dequeues the message and performs a synchronous, potentially blockingsend()call to transmit the data to the final destination.
This architecture is powerful because it isolates concerns:
- The Reactor is concerned with I/O efficiency and responsiveness.
- The Active Object is concerned with executing a task (sending data) without blocking the main event loop.
- The composition of these two (via the queue) creates the Half-Sync/Half-Async pattern, giving you the best of both worlds: a highly responsive async front-end and a simple, easy-to-reason-about sync back-end.
Conclusion
Today we've seen that design patterns are not just isolated solutions but are fundamental building blocks for creating sophisticated software architectures. Understanding how to compose them is what elevates your designs from simple programs to robust, scalable, and maintainable systems.
Key Takeaways:
- Patterns are compositional: Complex patterns are often built from simpler ones. The Active Object pattern is a prime example, combining Proxy, Command, a Monitor Object (the queue), and a Scheduler.
- Implementation reveals composition: Diving into the "under the hood" implementation of features like
std::functionreveals their foundation in patterns like Type Erasure (which itself is a composition of External Polymorphism, Bridge, and Prototype). - Architectural patterns emerge from composition: System-level patterns like Half-Sync/Half-Async are the result of combining tactical patterns like Reactor and Active Object to solve a larger problem—in this case, balancing I/O performance with programming simplicity.
- Intent matters: A key to successful composition is understanding the intent of each pattern. Knowing that the Reactor's intent is to demultiplex events and the Active Object's intent is to decouple invocation from execution helps you see why they fit together so well.
Next Lesson Preview:
This lesson concludes our module on concurrency and architectural patterns. You are now well-equipped to tackle the final portfolio project. In the first lesson of the next module, we will begin by defining a clean public API for our thread pool library and designing its internal architecture. We will draw directly on the patterns we've analyzed, especially the Monitor Object, Active Object, and the event loop logic, to lay the foundation for a professional-quality C++ component.