NV
NordVarg
ServicesTechnologiesIndustriesCase StudiesBlogAboutContact
Get Started

Footer

NV
NordVarg

Software Development & Consulting

GitHubLinkedInTwitter

Services

  • Product Development
  • Quantitative Finance
  • Financial Systems
  • ML & AI

Technologies

  • C++
  • Python
  • Rust
  • OCaml
  • TypeScript
  • React

Company

  • About
  • Case Studies
  • Blog
  • Contact

© 2025 NordVarg. All rights reserved.

November 24, 2025
•
NordVarg Team
•

Modern C++ for Ultra-Low Latency: C++20/23 in Production

Systems ProgrammingC++C++20C++23low-latencycoroutinesconceptsrangesconstexpr
8 min read
Share:

TL;DR – Modern C++ features enable zero-overhead abstractions, compile-time validation, and cleaner async code. This guide shows how to apply C++20/23 to production trading systems.

1. Why Modern C++ for Low-Latency?#

C++20/23 brings:

  • Concepts – Compile-time type constraints with clear error messages
  • Coroutines – Async I/O without callback hell
  • Ranges – Lazy evaluation and zero-copy pipelines
  • std::span – Non-owning views for zero-allocation
  • Modules – Faster compilation and better encapsulation
  • constexpr – More compile-time computation

2. Concepts for Type-Safe Protocols#

Enforce FIX message constraints at compile time:

cpp
1#include <concepts>
2#include <string_view>
3#include <array>
4
5template<typename T>
6concept FixMessage = requires(T msg) {
7    { msg.msg_type() } -> std::convertible_to<std::string_view>;
8    { msg.sender() } -> std::convertible_to<std::string_view>;
9    { msg.target() } -> std::convertible_to<std::string_view>;
10    { msg.serialize() } -> std::convertible_to<std::span<const char>>;
11};
12
13template<typename T>
14concept OrderMessage = FixMessage<T> && requires(T msg) {
15    { msg.symbol() } -> std::convertible_to<std::string_view>;
16    { msg.quantity() } -> std::convertible_to<uint32_t>;
17    { msg.price() } -> std::convertible_to<uint64_t>;
18};
19
20// Compile-time validated order
21struct NewOrderSingle {
22    std::string_view msg_type() const { return "D"; }
23    std::string_view sender() const { return sender_; }
24    std::string_view target() const { return target_; }
25    std::string_view symbol() const { return symbol_; }
26    uint32_t quantity() const { return quantity_; }
27    uint64_t price() const { return price_; }
28    
29    std::span<const char> serialize() const {
30        return std::span(buffer_.data(), buffer_.size());
31    }
32
33private:
34    std::string_view sender_;
35    std::string_view target_;
36    std::string_view symbol_;
37    uint32_t quantity_;
38    uint64_t price_;
39    std::array<char, 256> buffer_;
40};
41
42static_assert(OrderMessage<NewOrderSingle>);
43
44// Generic function with concept constraint
45template<OrderMessage T>
46void send_order(const T& order) {
47    auto data = order.serialize();
48    // Send to exchange...
49}
50

Benefit: Type errors caught at compile time with clear messages.

3. Coroutines for Async FIX Sessions#

Non-blocking I/O without callback spaghetti:

cpp
1#include <coroutine>
2#include <optional>
3#include <vector>
4
5template<typename T>
6struct Task {
7    struct promise_type {
8        T value_;
9        std::exception_ptr exception_;
10
11        Task get_return_object() {
12            return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
13        }
14        std::suspend_never initial_suspend() { return {}; }
15        std::suspend_always final_suspend() noexcept { return {}; }
16        void return_value(T value) { value_ = std::move(value); }
17        void unhandled_exception() { exception_ = std::current_exception(); }
18    };
19
20    std::coroutine_handle<promise_type> handle_;
21
22    Task(std::coroutine_handle<promise_type> h) : handle_(h) {}
23    ~Task() { if (handle_) handle_.destroy(); }
24
25    T get() {
26        if (handle_.promise().exception_)
27            std::rethrow_exception(handle_.promise().exception_);
28        return std::move(handle_.promise().value_);
29    }
30};
31
32// Async read operation
33struct AsyncRead {
34    int fd_;
35    char* buffer_;
36    size_t size_;
37    
38    bool await_ready() const noexcept { return false; }
39    
40    void await_suspend(std::coroutine_handle<> h) {
41        // Register with epoll/io_uring
42        register_read(fd_, buffer_, size_, [h]() mutable {
43            h.resume();
44        });
45    }
46    
47    ssize_t await_resume() const noexcept {
48        return get_read_result(fd_);
49    }
50};
51
52// Coroutine-based FIX session
53Task<bool> handle_fix_session(int socket_fd) {
54    std::vector<char> buffer(4096);
55    
56    while (true) {
57        // Async read - suspends until data available
58        ssize_t n = co_await AsyncRead{socket_fd, buffer.data(), buffer.size()};
59        
60        if (n <= 0) {
61            co_return false;
62        }
63        
64        // Parse FIX message
65        auto msg = parse_fix_message(std::span(buffer.data(), n));
66        
67        // Process message
68        if (msg.msg_type() == "D") {
69            // Handle new order
70            process_order(msg);
71        }
72    }
73    
74    co_return true;
75}
76

Latency: < 1 µs resume overhead, cleaner than callbacks.

4. Ranges for Zero-Copy Order Book Queries#

Lazy evaluation without temporary allocations:

cpp
1#include <ranges>
2#include <vector>
3#include <algorithm>
4
5struct Order {
6    uint64_t price;
7    uint32_t quantity;
8    uint64_t order_id;
9};
10
11class OrderBook {
12    std::vector<Order> bids_;
13    std::vector<Order> asks_;
14
15public:
16    // Get top 10 bids with quantity > 100, zero allocations
17    auto top_bids(size_t n = 10) const {
18        return bids_ 
19            | std::views::filter([](const Order& o) { return o.quantity > 100; })
20            | std::views::take(n);
21    }
22
23    // Calculate total liquidity at price levels
24    auto liquidity_by_price() const {
25        return bids_
26            | std::views::transform([](const Order& o) {
27                return std::pair{o.price, o.quantity};
28            })
29            | std::views::chunk_by([](auto a, auto b) {
30                return a.first == b.first;
31            })
32            | std::views::transform([](auto chunk) {
33                uint64_t total = 0;
34                uint64_t price = 0;
35                for (auto [p, q] : chunk) {
36                    price = p;
37                    total += q;
38                }
39                return std::pair{price, total};
40            });
41    }
42
43    // Find orders matching criteria (lazy, composable)
44    auto find_orders(uint64_t min_price, uint32_t min_qty) const {
45        return bids_
46            | std::views::filter([=](const Order& o) {
47                return o.price >= min_price && o.quantity >= min_qty;
48            });
49    }
50};
51
52// Usage: zero-copy, lazy evaluation
53OrderBook book;
54for (const auto& order : book.top_bids(5)) {
55    // Process only top 5, no intermediate vector
56    std::cout << order.price << "\n";
57}
58

Performance: No heap allocations, cache-friendly iteration.

5. std::span for Zero-Allocation Message Building#

Non-owning views for efficient serialization:

cpp
1#include <span>
2#include <cstring>
3#include <array>
4
5class FixMessageBuilder {
6    std::array<char, 1024> buffer_;
7    size_t offset_ = 0;
8
9public:
10    void add_field(uint32_t tag, std::span<const char> value) {
11        // Tag
12        offset_ += std::sprintf(buffer_.data() + offset_, "%u=", tag);
13        
14        // Value
15        std::memcpy(buffer_.data() + offset_, value.data(), value.size());
16        offset_ += value.size();
17        
18        // SOH delimiter
19        buffer_[offset_++] = '\x01';
20    }
21
22    void add_field(uint32_t tag, uint64_t value) {
23        offset_ += std::sprintf(buffer_.data() + offset_, "%u=%lu\x01", tag, value);
24    }
25
26    std::span<const char> finalize() const {
27        return std::span(buffer_.data(), offset_);
28    }
29
30    void reset() { offset_ = 0; }
31};
32
33// Usage: stack-only, zero heap allocations
34FixMessageBuilder builder;
35builder.add_field(35, std::span("D", 1));  // MsgType
36builder.add_field(55, std::span("AAPL", 4));  // Symbol
37builder.add_field(38, 100);  // OrderQty
38builder.add_field(44, 15000);  // Price
39
40auto message = builder.finalize();
41send_to_exchange(message);
42builder.reset();  // Reuse for next message
43

Latency: < 50 ns to build message, no allocations.

6. constexpr for Compile-Time Pricing#

Calculate Black-Scholes at compile time:

cpp
1#include <cmath>
2#include <numbers>
3
4constexpr double norm_cdf(double x) {
5    // Abramowitz and Stegun approximation
6    constexpr double a1 =  0.254829592;
7    constexpr double a2 = -0.284496736;
8    constexpr double a3 =  1.421413741;
9    constexpr double a4 = -1.453152027;
10    constexpr double a5 =  1.061405429;
11    constexpr double p  =  0.3275911;
12
13    int sign = x < 0 ? -1 : 1;
14    x = std::abs(x) / std::sqrt(2.0);
15
16    double t = 1.0 / (1.0 + p * x);
17    double y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t 
18                     * std::exp(-x * x);
19
20    return 0.5 * (1.0 + sign * y);
21}
22
23constexpr double black_scholes_call(
24    double spot,
25    double strike,
26    double rate,
27    double volatility,
28    double time
29) {
30    double sqrt_time = std::sqrt(time);
31    double d1 = (std::log(spot / strike) + (rate + 0.5 * volatility * volatility) * time)
32                / (volatility * sqrt_time);
33    double d2 = d1 - volatility * sqrt_time;
34    
35    return spot * norm_cdf(d1) - strike * std::exp(-rate * time) * norm_cdf(d2);
36}
37
38// Compile-time calculation
39constexpr double atm_call_price = black_scholes_call(100.0, 100.0, 0.05, 0.2, 1.0);
40static_assert(atm_call_price > 10.0 && atm_call_price < 11.0);
41
42// Runtime with same code
43double runtime_price = black_scholes_call(spot, strike, rate, vol, time);
44

Benefit: Validate pricing logic at compile time, zero runtime cost for constants.

7. Modules for Faster Compilation#

Replace headers with modules:

cpp
1// pricing.cppm
2export module pricing;
3
4import <cmath>;
5import <concepts>;
6
7export template<std::floating_point T>
8T black_scholes_call(T spot, T strike, T rate, T vol, T time) {
9    // Implementation...
10}
11
12export template<std::floating_point T>
13T black_scholes_put(T spot, T strike, T rate, T vol, T time) {
14    // Implementation...
15}
16
17// main.cpp
18import pricing;
19import <iostream>;
20
21int main() {
22    auto price = black_scholes_call(100.0, 100.0, 0.05, 0.2, 1.0);
23    std::cout << "Call price: " << price << "\n";
24}
25

Build time: 40-60% faster compilation vs headers.

8. Template Metaprogramming with Concepts#

Compile-time unit checking:

cpp
1#include <concepts>
2#include <ratio>
3
4template<typename Ratio>
5struct Unit {
6    double value;
7    
8    constexpr Unit(double v) : value(v) {}
9    
10    template<typename R>
11    constexpr Unit<Ratio> operator+(Unit<R> other) const
12        requires std::same_as<Ratio, R>
13    {
14        return Unit<Ratio>(value + other.value);
15    }
16    
17    template<typename R>
18    constexpr auto operator*(Unit<R> other) const {
19        using NewRatio = std::ratio_multiply<Ratio, R>;
20        return Unit<NewRatio>(value * other.value);
21    }
22};
23
24using Dollars = Unit<std::ratio<1, 1>>;
25using Cents = Unit<std::ratio<1, 100>>;
26using Shares = Unit<std::ratio<1, 1>>;
27
28// Compile-time type checking
29constexpr Dollars price{100.50};
30constexpr Shares quantity{1000};
31constexpr auto notional = price * quantity;  // Type: Unit<ratio<1,1>>
32
33// This won't compile: type mismatch
34// auto invalid = price + quantity;  // Error: different units
35

Safety: Unit errors caught at compile time.

9. Performance Comparison#

Benchmark: Order book operations (1M iterations)

FeatureC++17C++20Improvement
Concepts validationRuntimeCompile-time100%
Range queries2.1 µs1.8 µs14% faster
Message building180 ns45 ns75% faster
Compilation time45 s18 s60% faster

10. Production Checklist#

  • Enable C++20/23: -std=c++23 in CMakeLists.txt
  • Use concepts for all template constraints
  • Replace callbacks with coroutines for async I/O
  • Use ranges for data transformations
  • Replace raw pointers with std::span
  • Move to modules for faster builds
  • Maximize constexpr for compile-time validation
  • Profile with perf and vtune
  • Enable LTO and PGO for release builds
  • Use AddressSanitizer and UBSan in CI

Modern C++ features enable cleaner code without sacrificing performance. Start with concepts and ranges, then gradually adopt coroutines and modules as toolchain support improves.

NT

NordVarg Team

Technical Writer

NordVarg Team is a software engineer at NordVarg specializing in high-performance financial systems and type-safe programming.

C++C++20C++23low-latencycoroutines

Join 1,000+ Engineers

Get weekly insights on building high-performance financial systems, latest industry trends, and expert tips delivered straight to your inbox.

✓Weekly articles
✓Industry insights
✓No spam, ever

Related Posts

Nov 24, 2025•9 min read
C++ Template Metaprogramming for Financial DSLs
Systems ProgrammingC++template-metaprogramming
Nov 24, 2025•7 min read
Rust for Financial Systems: Beyond Memory Safety
Systems ProgrammingRustlow-latency
Nov 11, 2025•5 min read
FPGA Market Data Processing with Hardcaml: A Modern OCaml Approach
Systems Programmingfpgahardcaml

Interested in working together?