Reimagining Chain of Responsibility with Coroutines in C++20

Introduction

Design patterns are essential tools in a developer’s toolkit. Among them, the Chain of Responsibility Design Pattern stands out as a clean way to decouple request senders from receivers. Traditionally, this pattern is implemented using class hierarchies where each handler either processes the request or passes it down the chain. While effective, the classical approach can be verbose and inflexible.

With the advent of C++20 coroutines, we now have the power to rethink how we implement such patterns. Coroutines offer a lazy, resumable, and composable mechanism that naturally aligns with the idea of passing control across a chain. In this post, we’ll explore how to modernize the Chain of Responsibility pattern using coroutines, leading to more readable, testable, and flexible code.

Whether you're an intermediate developer or a seasoned C++ programmer, this article will show you how modern C++ features can revitalize well-known design patterns.


What is the Chain of Responsibility Design Pattern?

The Chain of Responsibility is a behavioral pattern where a request is passed along a chain of handlers. Each handler decides either to process the request or to pass it to the next handler.

Typical Use Cases

  • Logging systems

  • Event handling pipelines

  • UI frameworks

  • Middleware in web servers


Classical Implementation in C++

Let’s look at how this pattern is typically implemented using object-oriented principles.

#include <iostream>
#include <memory>
#include <string>

class Handler {
protected:
    std::shared_ptr<Handler> next;
public:
    void setNext(std::shared_ptr<Handler> handler) {
        next = handler;
    }

    virtual void handle(const std::string& request) {
        if (next) {
            next->handle(request);
        }
    }

    virtual ~Handler() = default;
};

class AuthHandler : public Handler {
public:
    void handle(const std::string& request) override {
        if (request == "auth") {
            std::cout << "AuthHandler handled the request\n";
        } else {
            Handler::handle(request);
        }
    }
};

class LogHandler : public Handler {
public:
    void handle(const std::string& request) override {
        if (request == "log") {
            std::cout << "LogHandler handled the request\n";
        } else {
            Handler::handle(request);
        }
    }
};

Usage:

auto auth = std::make_shared<AuthHandler>();
auto log = std::make_shared<LogHandler>();
auth->setNext(log);
auth->handle("log");

This works—but as chains grow, the boilerplate becomes tedious. We have to manually wire handlers and override behavior carefully. What if we could make it more dynamic?


What Are Coroutines in C++20?

Coroutines in C++20 allow functions to suspend and resume execution while retaining state. They're perfect for building pipelines, iterators, or tasks that may pause.

Useful Coroutines Concepts

  • co_yield: yield a value from a generator.

  • co_return: return from a coroutine.

  • co_await: wait for another coroutine to finish.

Coroutines remove the need for class hierarchies when managing control flow. Instead of hardcoding “what comes next,” each coroutine yields control back to the caller, enabling elegant chaining.

Want to learn coroutines in depth? Check out the official cppreference coroutine section.


Implementing Chain of Responsibility Using Coroutines

Let’s create a coroutine-based version of the pattern using generator-style coroutines.

We'll simulate a request handler pipeline using co_yield.

Step 1: Coroutine Infrastructure

We’ll use the coroutine support provided by the cppcoro or you can use generator from the Microsoft STL. But here’s a minimal example:

#include <coroutine>
#include <iostream>
#include <optional>
#include <string>

template<typename T>
struct Generator {
    struct promise_type;
    using handle_type = std::coroutine_handle<promise_type>;

    struct promise_type {
        T current_value;

        auto get_return_object() { return Generator{handle_type::from_promise(*this)}; }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        std::suspend_always yield_value(T value) {
            current_value = value;
            return {};
        }
        void return_void() {}
        void unhandled_exception() { std::exit(1); }
    };

    handle_type coro;

    Generator(handle_type h): coro(h) {}
    ~Generator() { if (coro) coro.destroy(); }

    T next() {
        coro.resume();
        return coro.promise().current_value;
    }

    bool done() const {
        return coro.done();
    }
};

Step 2: Define Handlers as Coroutines

Generator<std::string> AuthHandler(const std::string& request) {
    if (request == "auth") {
        co_yield "Handled by AuthHandler";
    } else {
        co_yield "";
    }
}

Generator<std::string> LogHandler(const std::string& request) {
    if (request == "log") {
        co_yield "Handled by LogHandler";
    } else {
        co_yield "";
    }
}

Step 3: Compose the Chain

void HandleRequest(const std::string& request) {
    for (auto result : { AuthHandler(request), LogHandler(request) }) {
        auto msg = result.next();
        if (!msg.empty()) {
            std::cout << msg << "\n";
            break;
        }
    }
}

Usage:

int main() {
    HandleRequest("log");
    HandleRequest("auth");
    HandleRequest("unknown");
}

Why This Works Better in Modern C++

Feature Classical CoR Coroutine-Based CoR
Verbosity       High (manual chaining)     Low (functional style)
Testability       Requires mocks     Easy to test individual coroutines
Flexibility        Static chain     Dynamic pipelines
Performance       Slight overhead     Optimized by the compiler
Async Support       Complex     Native via co_await

Turning It Async: Chain with co_await

For async workflows (network calls, file I/O), co_await allows suspension until a task completes.

#include <future>
#include <thread>

std::future<std::string> AsyncHandler(const std::string& request) {
    if (request == "async") {
        co_return "Handled asynchronously";
    }
    co_return "";
}

Usage:

auto future = AsyncHandler("async");
std::cout << future.get();  // Outputs: Handled asynchronously

In an advanced pipeline, each coroutine could call async services and forward the result. The pattern remains the same, but now your handlers are non-blocking, making them ideal for event-driven applications.


Real-World Use Case: Middleware Pipeline in a Server

Imagine a web server handling HTTP requests with middlewares: logging, authentication, and caching. Using coroutines:

Generator<std::string> MiddlewarePipeline(const std::string& request) {
    co_yield co_await AuthHandlerAsync(request);
    co_yield co_await LogHandlerAsync(request);
    co_yield co_await CachingHandlerAsync(request);
}

Learn more about async design in C++ from Lewis Baker’s CppCon talk on coroutines.


Common Pitfalls

  • Coroutine lifetime: Ensure your coroutine handle isn’t destroyed prematurely.

  • Stackless by default: Coroutines don’t keep call stacks, so debugging might need extra attention.

  • Standard Library limitations: Not all compilers have stable coroutine libraries—prefer MSVC or Clang for better support.


Conclusion

The Chain of Responsibility Design Pattern has been a staple in object-oriented programming for decades. With C++20 coroutines, we now have a more expressive, testable, and readable way to implement this pattern—without being tied to rigid class hierarchies.

This coroutine-based approach not only simplifies the codebase but also opens doors to async processing, making it ideal for modern applications like game engines, UI frameworks, and web servers.

Suggested Read : https://simplifyyourday.blogspot.com/2025/03/concepts-and-requires-in-cpp20.html


Key Takeaways

  • Coroutines enable clean and lazy chaining of handlers.

  • Greatly reduce boilerplate compared to classical inheritance-based CoR.

  • Seamlessly extend to asynchronous use cases using co_await.

  • A perfect use case for modern C++ development environments.


Further Reading



Comments

Popular posts from this blog

Step By Step Guide to Detect Heap Corruption in Windows Easily

Graph Visualization using MSAGL with Examples

How To Visualize Clustered and Unclustered Index In SQL