Modern C++ Toolkit for Design
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.
The Modern C++ Architecture Decision Matrix:
-
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.
- Use Virtual Interfaces (
-
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::visitwhen the set of types is closed (a fixed set of domain states or message types known at compile time).
-
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).
- Use
-
Memory Views & Safety:
- Pass
std::span<const T>instead ofconst std::vector<T>&for contiguous sequences without allocation. - Use
enum classto prevent implicit integer casting and namespace collisions.
- Pass
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.
The Modern C++ Architecture Decision Matrix:
-
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.
- Use Virtual Interfaces (
-
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::visitwhen the set of types is closed (a fixed set of domain states or message types known at compile time).
-
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).
- Use
-
Memory Views & Safety:
- Pass
std::span<const T>instead ofconst std::vector<T>&for contiguous sequences without allocation. - Use
enum classto prevent implicit integer casting and namespace collisions.
- Pass
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.
[[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.
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.
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.
[[nodiscard]] int computeTotalQuantity(std::span<const int> quantities) noexcept {
return std::accumulate(quantities.begin(), quantities.end(), 0);
}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.
- + orderId: string
- + quantity: int
- + priceRupees: int
- + tradeId: string
- + executedValue: int
- InsufficientFunds
- LimitExceeded
- CircuitBreaker
- + checkLimit(order: TradeOrder): bool
- + maxNotionalRupees: int
- + checkLimit(order: TradeOrder): bool
- - state_: variant<Placed, Filled, Rejected>
- + printStatus(): void
Runnable C++23 example
- 1.
Compile-Time Verification with C++20 Concepts:
RiskCheckerconstrains the type passed intoevaluateTrade.NotionalLimitCheckersatisfies the concept without inheriting from an abstract base class, allowing full compiler inlining. - 2.
Explicit Domain Failures with
std::expected(C++23):evaluateTradereturnsstd::expected<TradeReceipt, RejectReason>. Successful paths return aTradeReceipt, while insufficient funds or risk limit violations returnstd::unexpected(RejectReason), eliminating throwing exceptions for ordinary business branches. - 3.
Closed Polymorphism with
std::variant&std::visit: The order lifecycle is tracked viastd::variant<PlacedState, FilledState, RejectedState>. Transitioning states requires no heap allocation, andprintOrderStatususes theOverloadedvisitor pattern for exhaustive, type-safe matching. - 4.
Zero-Copy Sequence Views with
std::span(C++20):computeTotalQuantityaccepts astd::span<const int>. The same function seamlessly accepts astd::vector, astd::array, or a sub-slice without copying elements or forcing allocations.
#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;
}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
Modern C++ note
Throwing domain exceptions (throw PaymentException(...)) or returning error codes with out-parameters (bool pay(int amt, Receipt* out, string* err)).
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.
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.
Rules of thumb
Use
std::expectedfor domain validation, exceptions for system faults: If an operation can fail as part of regular business rules (insufficient balance, expired token, limit reached), returnstd::expected. Reserve exceptions for fatal system-level errors.Choose
std::variantfor closed type sets, virtual interfaces for open sets: If all possible subtypes are known upfront and you will never need runtime plugin extension,std::variantdelivers 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 ofconst 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 returningstd::expectedor status codes with[[nodiscard]]to prevent silent ignored failures.Use
enum classfor all enumerations: Scoped enums prevent namespace pollution and eliminate implicit conversions to integers that cause silent logic bugs.
Don't make this mistake
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.
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.
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.