| layout | default |
|---|---|
| class | overload-slide |
The if constexpr ladder (what the talk uses):
std::visit([](auto&& state) {
using T = decay_t<decltype(state)>;
if constexpr (is_same_v<T, NewOrder>) { ... }
else if constexpr (is_same_v<T, Working>) { ... }
else if constexpr (is_same_v<T, Filled>) { ... }
}, order.status);The overload pattern (alternative):
template<class... Ts> struct overload : Ts... { using Ts::operator()...; };
template<class... Ts> overload(Ts...) -> overload<Ts...>; // not needed in C++20
std::visit(overload{
[](const NewOrder& s) { ... },
[](const Working& s) { ... },
[](const Filled& s) { ... },
}, order.status);overloaduses normal overload resolution — nois_same_vordecay_t- Each lambda takes the concrete type directly — cleaner, no
auto&&gymnastics if constexprladder is more familiar;overloadis closer to pattern matching
- C++26: structured async programming model (P2300)
- Sender: describes a computation that will eventually produce a value
- Receiver: handles the result (value, error, or cancellation)
- Analogous to our
.and_thenpipeline — but for async and concurrent work
auto pipeline =
ex::just(order)
| ex::then(enrichWithMarketData)
| ex::then(validateOrder)
| ex::on(exchange_thread, applyFills);- Composable, lazy, zero-overhead abstraction
- Cancellation and error propagation are first-class
Hot paths with tight latency budgets:
std::functionvirtual dispatch: ~1–5ns per call — measurable at nanosecond scale- Prefer templates or raw function pointers in the critical path
- Profile first:
perf, VTune, or Tracy before assuming overhead
Embedded / -fno-rtti / -fno-exceptions environments:
std::functionmay be unavailable or too heavystd::variant+std::visitrequire RTTI in some implementations- Use simpler tagged union or template-based dispatch instead
Very small, stable, self-contained modules:
- A 200-line class that never changes and has no dependents — leave it alone
- Modernization has a cost: review time, testing, cognitive load for readers
- Pure functions are inherently thread-safe — no shared mutable state
OrderContext: read-only infrastructure bundle — safe to share across threads if members are const-correctstd::expectedreturn values: each thread gets its own value — no sharingvector<Transform>pipeline: safe to share if built before threads start, read-only after
Some caveats to pay attention to:
OrderContextmembers that lazily cache (e.g.,mutablecache fields) need synchronizationstd::functionitself is not thread-safe to copy/assign concurrently- Pipelines that accumulate into shared state need explicit coordination
- ABI (Application Binary Interface): binary contract between compiled units — vtable layout, name mangling, calling conventions
- Adding a method to a base class breaks ABI — all subclasses must recompile
- Type-erased wrappers (
AnyOrderProcessor,std::function) stabilize ABI at the wrapper boundary - Concrete types behind the wrapper can change freely without recompiling callers
std::function<void(const Order&)>is a stable symbol — swap the implementation, callers unchanged
Watch out for:
std::expected,std::variantlayout changes between standard library versions- Mixing compilers or STL versions across shared library boundaries
- Stores any callable matching a signature via type erasure
- Small Buffer Optimization (SBO): small callables (≤ ~16–32 bytes) stored inline — no heap allocation
- Large captures or stateful functors heap-allocate on construction
- Every call goes through a virtual dispatch indirection (function pointer table)
std::move_only_function(C++23): no copy requirement, better for unique ownership
std::function |
Raw pointer | Template | |
|---|---|---|---|
| Heap alloc | Maybe | No | No |
| Overhead | Virtual dispatch | None | Inlined |
| Flexibility | Any callable | Fixed sig | Fixed at compile time |