2Brute2Best
πŸ”₯0
F7

Modern C++ Toolkit for Design

C++20/23 Idioms concepts std::variant std::expected std::print std::span
⚑ 60-Second Architecture SummaryF7 · Foundation
4 Core ConceptsC++23 Verified Demo5 Rules of Thumb
Why It Matters in High-Scale Systems

For decades, C++ Low-Level Design relied almost exclusively on heavy object-oriented machinery: deep inheritance hierarchies, heap-allocated virtual interfaces, and runtime exceptions for error handling. Modern C++ (C++20 and C++23) fundamentally modernizes this toolkit. With std::expected<T, E>, expected business failures (insufficient funds, risk limit breaches) are modeled explicitly in return types without the latency spikes and invisible control flow of exceptions. With std::variant and std::visit, closed state machines achieve type-safe polymorphism with zero heap allocation and zero vtable pointers. With C++20 concepts, static contracts enable compiler inlining while preserving clean abstractions. Knowing when to deploy these modern idioms versus classic virtual interfaces is what separates modern C++ architects from legacy developers.

Core Mental Model

The Modern C++ Architecture Decision Matrix:

  1. Dynamic vs Static Contracts:

    • Use Virtual Interfaces (unique_ptr<IStrategy>) when strategies are plugged in dynamically at runtime based on user input or configuration.
    • Use C++20 Concepts (template <RiskChecker T>) when algorithms are fixed at compile time and you want compiler inlining and zero heap overhead.
  2. Open vs Closed Polymorphism:

    • Use Virtual Base Classes when the type hierarchy is open (third parties or plugins must add new subclasses without modifying existing code).
    • Use std::variant + std::visit when the set of types is closed (a fixed set of domain states or message types known at compile time).
  3. Failure Handling:

    • Use std::expected<T, E> (C++23) for expected domain failures (e.g. invalid coupon, balance insufficient, risk rejected).
    • Reserve Exceptions for truly unexpected catastrophic infrastructure faults (out of memory, corrupted hardware, broken socket).
  4. Memory Views & Safety:

    • Pass std::span<const T> instead of const std::vector<T>& for contiguous sequences without allocation.
    • Use enum class to prevent implicit integer casting and namespace collisions.
01

Why this matters in LLD interviews

For decades, C++ Low-Level Design relied almost exclusively on heavy object-oriented machinery: deep inheritance hierarchies, heap-allocated virtual interfaces, and runtime exceptions for error handling. Modern C++ (C++20 and C++23) fundamentally modernizes this toolkit. With std::expected<T, E>, expected business failures (insufficient funds, risk limit breaches) are modeled explicitly in return types without the latency spikes and invisible control flow of exceptions. With std::variant and std::visit, closed state machines achieve type-safe polymorphism with zero heap allocation and zero vtable pointers. With C++20 concepts, static contracts enable compiler inlining while preserving clean abstractions. Knowing when to deploy these modern idioms versus classic virtual interfaces is what separates modern C++ architects from legacy developers.

Mental model

The Modern C++ Architecture Decision Matrix:

  1. Dynamic vs Static Contracts:

    • Use Virtual Interfaces (unique_ptr<IStrategy>) when strategies are plugged in dynamically at runtime based on user input or configuration.
    • Use C++20 Concepts (template <RiskChecker T>) when algorithms are fixed at compile time and you want compiler inlining and zero heap overhead.
  2. Open vs Closed Polymorphism:

    • Use Virtual Base Classes when the type hierarchy is open (third parties or plugins must add new subclasses without modifying existing code).
    • Use std::variant + std::visit when the set of types is closed (a fixed set of domain states or message types known at compile time).
  3. Failure Handling:

    • Use std::expected<T, E> (C++23) for expected domain failures (e.g. invalid coupon, balance insufficient, risk rejected).
    • Reserve Exceptions for truly unexpected catastrophic infrastructure faults (out of memory, corrupted hardware, broken socket).
  4. Memory Views & Safety:

    • Pass std::span<const T> instead of const std::vector<T>& for contiguous sequences without allocation.
    • Use enum class to prevent implicit integer casting and namespace collisions.
02

Core concepts

2.1Domain Failures with std::expected<T, E> (C++23)

Exceptions in C++ are designed for exceptional errors, not ordinary business control flow. Throwing an exception on every failed payment or invalid trade destroys predictable latency and creates hidden exit paths across stack frames. Returning raw error codes with out-parameters is equally clumsy. In C++23, std::expected<Value, Error> makes failure explicit in the function signature. The caller is forced to handle the error, and success/failure data are stored in the same memory footprint without heap allocations.

C++23 snippet
[[nodiscard]] std::expected<TradeReceipt, RejectReason>
evaluateTrade(const TradeOrder& order, const RiskChecker auto& checker, int balance) {
    if (order.quantity * order.priceRupees > balance) {
        return std::unexpected(RejectReason::InsufficientFunds);
    }
    return TradeReceipt{ .tradeId = "TRD-1", .executedValue = 1000 };
}

2.2Closed Polymorphism: std::variant and std::visit

When a domain entity transitions through a fixed set of states (e.g. Placed, Filled, Rejected), classic design often creates an abstract base class OrderState and three derived heap classes. This introduces heap allocations, cache misses, and vtable pointer overhead. std::variant provides value-semantic polymorphism on the stack. Combined with std::visit and an overloaded lambda pattern, the compiler enforces exhaustive pattern matching across all states.

C++23 snippet
using OrderState = std::variant<PlacedState, FilledState, RejectedState>;

std::visit(Overloaded{
    [](const PlacedState& s) { /* ... */ },
    [](const FilledState& s) { /* ... */ },
    [](const RejectedState& s) { /* ... */ }
}, currentState);

2.3Compile-Time Contracts with C++20 Concepts

In classic C++, generic templates suffered from cryptic compiler error messages when a type lacked a required method. C++20 concept defines precise syntactic and semantic requirements for template arguments. If a risk checker or matching engine type is known at compile time, constraining it with a concept gives you the contract safety of an interface with the performance of inlined static dispatch.

C++23 snippet
template <typename T>
concept RiskChecker = requires(T checker, const TradeOrder& order) {
    { checker.checkLimit(order) } -> std::convertible_to<bool>;
};

2.4Zero-Copy Views with std::span (C++20)

Taking const std::vector<int>& in a utility method forces callers using std::array, C arrays, or memory-mapped buffers to allocate and copy into a std::vector. std::span<const T> provides a lightweight, non-owning view over any contiguous memory buffer (pointer + size), enabling callers to pass vectors, arrays, or sub-slices with zero copying.

C++23 snippet
[[nodiscard]] int computeTotalQuantity(std::span<const int> quantities) noexcept {
    return std::accumulate(quantities.begin(), quantities.end(), 0);
}
03

UML class structure

RiskChecker concept defines the compile-time contract for NotionalLimitChecker. The engine evaluates TradeOrder, returning expected<TradeReceipt, RejectReason>. OrderState holds a variant over Placed, Filled, and Rejected.

Β«structΒ»
TradeOrder
Domain value type
  • + orderId: string
  • + quantity: int
  • + priceRupees: int
Used by:RiskChecker(β‡’)
Β«structΒ»
TradeReceipt
Successful expected payload
  • + tradeId: string
  • + executedValue: int
Used by:OrderState(β‡’)
Β«enumΒ»
RejectReason
Scoped enum class error type
  • InsufficientFunds
  • LimitExceeded
  • CircuitBreaker
Used by:OrderState(β‡’)
Β«interfaceΒ»
RiskChecker
C++20 Concept contract
β‡’ depends onTradeOrder
  • + checkLimit(order: TradeOrder): bool
Used by:NotionalLimitChecker(β–³)
Β«structΒ»
NotionalLimitChecker
Satisfies RiskChecker concept
β–³ implementsRiskChecker
  • + maxNotionalRupees: int
  • + checkLimit(order: TradeOrder): bool
Β«structΒ»
OrderState
std::variant<Placed, Filled, Rejected>
⇒ depends onTradeReceipt⇒ depends onRejectReason
  • - state_: variant<Placed, Filled, Rejected>
  • + printStatus(): void
Relationships (UML arrows)
NotionalLimitCheckerβ•Œβ•Œβ–·RiskCheckersatisfies concept
RiskCheckerβ•Œβ•Œβ–ΆTradeOrdervalidates
OrderStateβ•Œβ•Œβ–ΆTradeReceiptholds filled trade
OrderStateβ•Œβ•Œβ–ΆRejectReasonholds reject reason
04

Runnable C++23 example

What the demo shows
  1. 1.

    Compile-Time Verification with C++20 Concepts: RiskChecker constrains the type passed into evaluateTrade. NotionalLimitChecker satisfies the concept without inheriting from an abstract base class, allowing full compiler inlining.

  2. 2.

    Explicit Domain Failures with std::expected (C++23): evaluateTrade returns std::expected<TradeReceipt, RejectReason>. Successful paths return a TradeReceipt, while insufficient funds or risk limit violations return std::unexpected(RejectReason), eliminating throwing exceptions for ordinary business branches.

  3. 3.

    Closed Polymorphism with std::variant & std::visit: The order lifecycle is tracked via std::variant<PlacedState, FilledState, RejectedState>. Transitioning states requires no heap allocation, and printOrderStatus uses the Overloaded visitor pattern for exhaustive, type-safe matching.

  4. 4.

    Zero-Copy Sequence Views with std::span (C++20): computeTotalQuantity accepts a std::span<const int>. The same function seamlessly accepts a std::vector, a std::array, or a sub-slice without copying elements or forcing allocations.

Foundation Demo Β· demo.cpp
#include <array>
#include <concepts>
#include <expected>
#include <numeric>
#include <print>
#include <span>
#include <string>
#include <string_view>
#include <variant>
#include <vector>

// 1. Scoped Enum (enum class) for strongly typed error reasons
enum class RejectReason {
    InsufficientFunds,
    LimitExceeded,
    CircuitBreaker
};

constexpr std::string_view toString(RejectReason reason) noexcept {
    switch (reason) {
        case RejectReason::InsufficientFunds: return "InsufficientFunds";
        case RejectReason::LimitExceeded:     return "LimitExceeded";
        case RejectReason::CircuitBreaker:    return "CircuitBreaker";
    }
    return "Unknown";
}

// Domain value types
struct TradeOrder {
    std::string orderId;
    int quantity;
    int priceRupees;
};

struct TradeReceipt {
    std::string tradeId;
    int executedValue;
};

// 2. C++20 Concept for compile-time contract checking (static polymorphism)
template <typename T>
concept RiskChecker = requires(T checker, const TradeOrder& order) {
    { checker.checkLimit(order) } -> std::convertible_to<bool>;
};

struct NotionalLimitChecker {
    int maxNotionalRupees;

    bool checkLimit(const TradeOrder& order) const noexcept {
        return (order.quantity * order.priceRupees) <= maxNotionalRupees;
    }
};

// 3. std::expected<T, E> (C++23) for explicit non-throwing domain failure handling
[[nodiscard]] std::expected<TradeReceipt, RejectReason>
evaluateTrade(const TradeOrder& order, const RiskChecker auto& checker, int accountBalance) {
    int notional = order.quantity * order.priceRupees;

    if (notional > accountBalance) {
        return std::unexpected(RejectReason::InsufficientFunds);
    }
    if (!checker.checkLimit(order)) {
        return std::unexpected(RejectReason::LimitExceeded);
    }

    return TradeReceipt{
        .tradeId = "TRD-" + order.orderId,
        .executedValue = notional
    };
}

// 4. Closed Polymorphism: std::variant + std::visit for Order State Machine
struct PlacedState {
    std::string placedAt;
};

struct FilledState {
    std::string tradeId;
    int filledValue;
};

struct RejectedState {
    RejectReason reason;
};

using OrderState = std::variant<PlacedState, FilledState, RejectedState>;

// Helper for visitor overload pattern
template <class... Ts>
struct Overloaded : Ts... {
    using Ts::operator()...;
};

void printOrderStatus(const OrderState& state) {
    std::visit(Overloaded{
        [](const PlacedState& s) {
            std::println("Status: PLACED at {}", s.placedAt);
        },
        [](const FilledState& s) {
            std::println("Status: FILLED (Trade: {}, Value: Rs.{})", s.tradeId, s.filledValue);
        },
        [](const RejectedState& s) {
            std::println("Status: REJECTED (Reason: {})", toString(s.reason));
        }
    }, state);
}

// 5. std::span (C++20) for non-owning view over contiguous sequences
[[nodiscard]] int computeTotalQuantity(std::span<const int> quantities) noexcept {
    return std::accumulate(quantities.begin(), quantities.end(), 0);
}

int main() {
    NotionalLimitChecker checker{.maxNotionalRupees = 500'000};

    // Test 1: Successful trade returning std::expected value
    TradeOrder order1{.orderId = "ORD-101", .quantity = 100, .priceRupees = 2500};
    auto result1 = evaluateTrade(order1, checker, 300'000);

    OrderState state1 = PlacedState{"10:00:00 UTC"};
    printOrderStatus(state1);

    if (result1.has_value()) {
        std::println("Order approved: {} for Rs.{}", result1->tradeId, result1->executedValue);
        state1 = FilledState{result1->tradeId, result1->executedValue};
        printOrderStatus(state1);
    }

    // Test 2: Rejected trade returning std::unexpected
    TradeOrder order2{.orderId = "ORD-102", .quantity = 300, .priceRupees = 2500};
    auto result2 = evaluateTrade(order2, checker, 100'000); // 750,000 exceeds balance and limit

    if (!result2.has_value()) {
        std::println("Order rejected: {}", toString(result2.error()));
        OrderState state2 = RejectedState{result2.error()};
        printOrderStatus(state2);
    }

    // Test 3: std::span with vector and array
    std::vector<int> batchVector = {50, 100, 150};
    std::array<int, 2> batchArray = {200, 300};

    std::println("Vector total qty: {}", computeTotalQuantity(batchVector));
    std::println("Array total qty: {}", computeTotalQuantity(batchArray));
    std::println("Subspan qty: {}", computeTotalQuantity(std::span(batchVector).subspan(1)));

    return 0;
}
Program output (stdout)
Status: PLACED at 10:00:00 UTC
Order approved: TRD-ORD-101 for Rs.250000
Status: FILLED (Trade: TRD-ORD-101, Value: Rs.250000)
Order rejected: InsufficientFunds
Status: REJECTED (Reason: InsufficientFunds)
Vector total qty: 300
Array total qty: 500
Subspan qty: 250
05

Modern C++ note

std::expected<T, E> for Domain FailuresC++23
What it replaces

Throwing domain exceptions (throw PaymentException(...)) or returning error codes with out-parameters (bool pay(int amt, Receipt* out, string* err)).

Why it is better

Error handling is explicit in the type signature with zero exception overhead. Callers cannot accidentally ignore failed conditions, linear control flow is preserved, and performance remains deterministic in low-latency systems.

When classic virtual wins

Catastrophic system crashes and unrecoverable infrastructure failures (e.g., out-of-memory errors or corrupted disk files) where immediate stack unwinding to a top-level crash handler is intended.

06

Rules of thumb

  • Use std::expected for domain validation, exceptions for system faults: If an operation can fail as part of regular business rules (insufficient balance, expired token, limit reached), return std::expected. Reserve exceptions for fatal system-level errors.

  • Choose std::variant for closed type sets, virtual interfaces for open sets: If all possible subtypes are known upfront and you will never need runtime plugin extension, std::variant delivers stack locality and zero vtable overhead. If new classes will be added dynamically by third parties, use a virtual interface.

  • Accept std::span<const T> instead of const std::vector<T>& for reads: It allows callers to pass vectors, arrays, stack buffers, or slices with zero allocations and zero copies.

  • Mark failure-returning functions with [[nodiscard]]: Always annotate functions returning std::expected or status codes with [[nodiscard]] to prevent silent ignored failures.

  • Use enum class for all enumerations: Scoped enums prevent namespace pollution and eliminate implicit conversions to integers that cause silent logic bugs.

07

Don't make this mistake

Throwing exceptions for expected business rule rejections
βœ— Receipt pay(int amount) { if (balance_ < amount) throw InsufficientFundsException(); // ... }
βœ“ [[nodiscard]] std::expected<Receipt, PaymentError> pay(int amount) { if (balance_ < amount) return std::unexpected(PaymentError::InsufficientFunds); // ... }

Throwing and catching exceptions in C++ involves runtime table lookups and stack unwinding, which can be 100x slower than normal returns and obscure control flow in business logic.

Using std::variant when the hierarchy is open to external extension
βœ— // Using variant for plugins where users add new strategies: using Strategy = std::variant<StrategyA, StrategyB>;
βœ“ // Use pure virtual interface when new strategies are added dynamically: class IStrategy { virtual ~IStrategy() = default; virtual void execute() = 0; };

Every time a new type is added to a std::variant, every std::visit across the entire codebase must be updated and recompiled. For open hierarchies, virtual interfaces are the right pattern.

Passing const std::vector<T>& for read-only contiguous sequences
βœ— int sum(const std::vector<int>& data); // Rejects std::array, raw pointers, sub-slices without copying!
βœ“ int sum(std::span<const int> data); // Accepts vector, array, raw buffer, subspan without copying

const std::vector<T>& tightly couples the function to std::vector. If the caller has a std::array or raw buffer, they must allocate a new vector just to call your function.

08

Quick interview check

09

Mark your progress

Systems Integration

Related C++ Systems & Algorithmic Foundations

Deep dive into underlying layers