Go back

Effective Modern C++: Writing Clean, Bug-Free, and High-Performance Code

Published:

Effective Modern C++

Table of Contents

Open Table of Contents

Introduction

Like many of you, I’ve been on a long journey with C++. From the early days of C++98 to the modern era of C++23, I’ve watched this language evolve into a powerful tool that can deliver both performance and expressiveness. Today, I want to share my insights on how to leverage C++23 features to write code that’s not only fast but also clean and reliable.

The C++23 Revolution

C++23 builds on the modern C++ philosophy that began with C++11, bringing us even more tools to write expressive, efficient, and safe code. As someone who’s worked across game development, financial systems, and low-level infrastructure, I’ve found that these new features aren’t just academic curiosities—they solve real problems.

📌 Key Takeaway: C++23 isn’t just about new syntax—it’s about writing code that’s easier to maintain, harder to misuse, and runs just as fast (if not faster) than older approaches.

When I first started exploring C++23, I was skeptical about whether the new features would actually improve my daily coding life. But after implementing them in several projects, I’ve become a convert. Let me show you why.

Writing Clean Code with C++23

The Power of std::expected

One of my favorite additions is std::expected, which provides a clean way to handle operations that might fail without resorting to exceptions or error codes:

#include <expected>
#include <string>
#include <filesystem>

std::expected<std::string, std::error_code> readFileContent(const std::filesystem::path& path) {
    if (!std::filesystem::exists(path)) {
        return std::unexpected(std::make_error_code(std::errc::no_such_file_or_directory));
    }

    // Read file and return content
    // ...
    return "File content here";
}

void processFile() {
    auto result = readFileContent("data.txt");
    if (result) {
        // Use the value
        std::string content = *result;
        // Process content...
    } else {
        // Handle the error
        std::error_code error = result.error();
        // Log or handle error...
    }
}

This pattern is much cleaner than traditional error handling methods. In financial systems I’ve worked on, this approach reduced our error-handling boilerplate by nearly 30% while making the intent of our code much clearer.

String Improvements: contains(), starts_with(), and ends_with()

String handling has always been verbose in C++. The new methods make common operations much more readable:

std::string log_line = "[ERROR] Database connection failed: timeout";

// Old way
if (log_line.find("ERROR") != std::string::npos) { /* ... */ }

// New way
if (log_line.contains("ERROR")) { /* ... */ }

// Similarly:
if (log_line.starts_with("[ERROR]")) { /* ... */ }
if (log_line.ends_with("timeout")) { /* ... */ }

These simple improvements have made my log parsing code much more readable, especially in our system monitoring tools where string operations are frequent.

Modules: A Cleaner Alternative to Headers

One of the biggest improvements for large codebases is the introduction of modules:

// math.cppm - a module interface file
export module math;

export int add(int a, int b) {
    return a + b;
}

export int subtract(int a, int b) {
    return a - b;
}
// main.cpp
import math;

int main() {
    int result = add(5, 3);  // No need for math:: prefix if not in a namespace
    return 0;
}

Modules address many issues with the traditional header system:

⚠️ Caveat: Module support varies between compilers. As of early 2025, GCC, Clang, and MSVC all support modules, but with some differences in implementation details. Check your compiler documentation before relying heavily on this feature.

Formatting Library

The <format> library brings Python-like string formatting to C++:

#include <format>
#include <string>

std::string name = "Alice";
int age = 30;
float height = 1.75f;

// Old way
std::string info = "Name: " + name + ", Age: " + std::to_string(age) +
                   ", Height: " + std::to_string(height) + "m";

// New way
std::string info = std::format("Name: {}, Age: {}, Height: {:.2f}m",
                               name, age, height);

// C++23 also allows for named arguments
std::string info2 = std::format("Name: {name}, Age: {age}, Height: {height:.2f}m",
                                std::format_args{{"name", name}, {"age", age},
                                {"height", height}});

This has been a game-changer for our logging and user-facing message systems. The code is not only cleaner but also safer, as format errors are caught at compile time.

Writing Bug-Free Code

Constexpr Everything

C++23 makes constexpr even more powerful, allowing us to perform more computations at compile time:

constexpr int fibonacci(int n) {
    if (n <= 1) return n;
    return fibonacci(n-1) + fibonacci(n-2);
}

// Array size determined at compile time
constexpr int fib10 = fibonacci(10);
int array[fib10];  // Creates an array of size 55

I’ve used this extensively in game development, where precalculating lookup tables at compile time can save valuable runtime performance.

Contracts (C++26, but worth mentioning)

While full contract support is expected in C++26, I want to highlight this upcoming feature since it aligns perfectly with bug-free programming:

// Future syntax (approximation)
int divide(int a, int b)
    [[pre: b != 0]]  // Precondition
    [[post r: r * b == a]]  // Postcondition
{
    return a / b;
}

Until this is available, we can simulate some of this behavior with assertions and static analysis tools.

std::span: Safer Array Handling

std::span provides a non-owning view into a contiguous sequence, making array operations safer:

#include <span>
#include <vector>

void processFirstFive(std::span<int> values) {
    // Only process up to 5 elements, but don't fail if fewer are provided
    for (size_t i = 0; i < std::min(values.size(), size_t{5}); ++i) {
        values[i] *= 2;
    }
}

int main() {
    std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8};

    // No need to pass begin/end iterators or size separately
    processFirstFive(data);

    // Can also work with C-style arrays, array subsets, etc.
    int raw_array[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    processFirstFive(std::span(raw_array, 10));

    // Or just a portion of an array
    processFirstFive(std::span(data).subspan(2, 5));

    return 0;
}

In a financial trading system I worked on, using std::span for array operations reduced buffer overflow bugs by an impressive margin.

Habit-Building for Bug-Free Code

Beyond specific features, here are some habits I’ve developed for bug-free C++ code:

HabitDescriptionBenefit
Always initializeUse constructors, member initializer lists, or aggregate initializationEliminates undefined behavior from uninitialized values
Use structured bindingsauto [iter, success] = map.insert(...)Clearer code, can’t forget to check success values
Leverage the type systemMake invalid states unrepresentableCompile-time safety
Use RAII universallyResources managed by object lifetimesNo leaks, even with exceptions
Write unit testsEspecially for edge casesCatches bugs before they ship

High-Performance Programming

Ranges and Views

C++20 introduced ranges, and C++23 continues to enhance them. They allow for expressive, pipe-based operations on sequences:

#include <ranges>
#include <vector>
#include <iostream>
#include <algorithm>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // Chain operations in a readable way
    auto result = numbers
        | std::views::filter([](int n) { return n % 2 == 0; })  // Even numbers
        | std::views::transform([](int n) { return n * n; })    // Square them
        | std::views::take(3);                                 // Take first three

    // Convert to vector for storage
    std::vector<int> final_results(result.begin(), result.end());

    // Prints: 4 16 36
    for (int n : final_results) {
        std::cout << n << " ";
    }

    return 0;
}

The beauty of ranges is that they’re lazy—operations aren’t performed until needed, making them efficient for large datasets.

Coroutines for Asynchronous Code

C++20 introduced coroutines, and they’ve been further refined in C++23. They’re particularly useful for asynchronous lazily-evaluated code:

#include <coroutine>
#include <future>
#include <iostream>

// A simplified co_future implementation
template<typename T>
struct co_future {
    struct promise_type {
        std::promise<T> promise;

        co_future get_return_object() {
            return co_future(promise.get_future());
        }

        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }

        void return_value(T value) {
            promise.set_value(std::move(value));
        }

        void unhandled_exception() {
            promise.set_exception(std::current_exception());
        }
    };

    std::future<T> future;

    explicit co_future(std::future<T> f) : future(std::move(f)) {}

    T get() { return future.get(); }
};

// A coroutine that performs a computation
co_future<int> compute() {
    // Simulate a long computation
    // In a real application, you might use co_await here
    co_return 42;
}

int main() {
    auto future = compute();
    std::cout << "Result: " << future.get() << std::endl;
    return 0;
}

In a network service I built, coroutines reduced the complexity of our async code dramatically, making it both more maintainable and more efficient.

Leveraging std::mdspan for Multi-dimensional Arrays

C++23 introduces std::mdspan, which provides a powerful way to work with multi-dimensional arrays:

#include <mdspan>
#include <vector>
#include <iostream>

void matrixMultiply(
    std::mdspan<const float, std::extents<size_t,
                std::dynamic_extent, std::dynamic_extent>> a,
    std::mdspan<const float, std::extents<size_t,
                std::dynamic_extent, std::dynamic_extent>> b,
    std::mdspan<float, std::extents<size_t,
                std::dynamic_extent, std::dynamic_extent>> result) {

    size_t m = a.extent(0);
    size_t n = b.extent(1);
    size_t p = a.extent(1);

    for (size_t i = 0; i < m; ++i) {
        for (size_t j = 0; j < n; ++j) {
            float sum = 0.0f;
            for (size_t k = 0; k < p; ++k) {
                sum += a[i, k] * b[k, j];
            }
            result[i, j] = sum;
        }
    }
}

int main() {
    // Underlying storage
    std::vector<float> a_data = {1, 2, 3, 4, 5, 6};
    std::vector<float> b_data = {7, 8, 9, 10, 11, 12};
    std::vector<float> result_data(4);

    // Create views with different layouts
    auto a = std::mdspan<float, std::extents<size_t, 2, 3>>(a_data.data());
    auto b = std::mdspan<float, std::extents<size_t, 3, 2>>(b_data.data());
    auto result = std::mdspan<float, std::extents<size_t, 2, 2>>(result_data.data());

    matrixMultiply(a, b, result);

    // Print result
    for (size_t i = 0; i < 2; ++i) {
        for (size_t j = 0; j < 2; ++j) {
            std::cout << result[i, j] << " ";
        }
        std::cout << std::endl;
    }

    return 0;
}

This feature allows clean, efficient multi-dimensional array operations without copying data. Note that, as of the time of this writing, this feature lacks widespread support in compilers.

Performance Best Practices

Here’s a quick reference table of my go-to performance best practices:

PracticeExampleBenefit
Avoid unnecessary allocationsUse reserve() for vectorsReduces memory allocations
Move semanticsauto result = std::move(expensive_object)Avoids deep copies
Return value optimizationReturn objects directly, not pointersCompiler can optimize better
Use std::string_view for string parametersvoid process(std::string_view s)Avoids string copies
Consider compiler optimizations-O3, profile-guided optimizationTailors code to specific use cases
Use standard algorithmsstd::transform instead of manual loopsOften faster, clearer intent

Real-World Examples

Game Development: Entity Component System

In game development, performance and clean architecture are both crucial. Here’s a simplified ECS using modern C++:

#include <vector>
#include <unordered_map>
#include <typeindex>
#include <memory>
#include <any>

// Component base
struct Component {};

// Sample components
struct Position : Component {
    float x, y, z;
};

struct Velocity : Component {
    float dx, dy, dz;
};

// Entity class
class Entity {
private:
    std::unordered_map<std::type_index, std::any> components;

public:
    template<typename T, typename... Args>
    void addComponent(Args&&... args) {
        static_assert(std::is_base_of_v<Component, T>, "T must derive from Component");
        components[typeid(T)] = T{std::forward<Args>(args)...};
    }

    template<typename T>
    T& getComponent() {
        static_assert(std::is_base_of_v<Component, T>, "T must derive from Component");
        return std::any_cast<T&>(components.at(typeid(T)));
    }

    template<typename T>
    bool hasComponent() const {
        static_assert(std::is_base_of_v<Component, T>, "T must derive from Component");
        return components.contains(typeid(T));
    }
};

// System that operates on entities with specific components
void physicsSystem(std::vector<Entity>& entities, float dt) {
    for (auto& entity : entities) {
        if (entity.hasComponent<Position>() && entity.hasComponent<Velocity>()) {
            auto& position = entity.getComponent<Position>();
            auto& velocity = entity.getComponent<Velocity>();

            // Update position based on velocity
            position.x += velocity.dx * dt;
            position.y += velocity.dy * dt;
            position.z += velocity.dz * dt;
        }
    }
}

This pattern leverages modern C++ features to create a flexible, type-safe entity system. In a real game engine, we’d optimize this further, perhaps using a more cache-friendly data layout.

Financial Systems: High-Performance Order Book

In financial systems, microseconds matter. Here’s a simplified order book implementation using modern C++:

#include <map>
#include <unordered_map>
#include <string>
#include <optional>

enum class Side { Buy, Sell };

struct Order {
    std::string id;
    double price;
    int quantity;
    Side side;
};

class OrderBook {
private:
    // Price-ordered maps for quick best bid/ask
    std::map<double, int, std::greater<double>> bids;  // Descending order for bids
    std::map<double, int> asks;  // Ascending order for asks

    // Fast lookup by order ID
    std::unordered_map<std::string, Order> orders;

public:
    bool placeOrder(const Order& order) {
        // Store in ID lookup
        orders[order.id] = order;

        // Add to price level
        auto& book = (order.side == Side::Buy) ? bids : asks;
        book[order.price] += order.quantity;

        return true;
    }

    bool cancelOrder(const std::string& order_id) {
        auto it = orders.find(order_id);
        if (it == orders.end()) {
            return false;
        }

        const Order& order = it->second;
        auto& book = (order.side == Side::Buy) ? bids : asks;

        // Remove from price level
        book[order.price] -= order.quantity;
        if (book[order.price] <= 0) {
            book.erase(order.price);
        }

        // Remove from ID lookup
        orders.erase(it);

        return true;
    }

    std::optional<double> bestBid() const {
        if (bids.empty()) return std::nullopt;
        return bids.begin()->first;
    }

    std::optional<double> bestAsk() const {
        if (asks.empty()) return std::nullopt;
        return asks.begin()->first;
    }

    std::optional<double> spread() const {
        auto bid = bestBid();
        auto ask = bestAsk();

        if (bid && ask) {
            return *ask - *bid;
        }

        return std::nullopt;
    }
};

In a production system, I’d further optimize with custom memory allocators and more sophisticated data structures, but this demonstrates how modern C++ features can make even performance-critical code readable.

Systems Programming: Thread Pool

For system-level programming, here’s a thread pool using C++23 features:

#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <functional>
#include <future>
#include <vector>

class ThreadPool {
private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;

    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;

public:
    ThreadPool(size_t threads) : stop(false) {
        for (size_t i = 0; i < threads; ++i) {
            workers.emplace_back([this] {
                while (true) {
                    std::function<void()> task;

                    {
                        std::unique_lock<std::mutex> lock(this->queue_mutex);
                        this->condition.wait(lock, [this] {
                            return this->stop || !this->tasks.empty();
                        });

                        if (this->stop && this->tasks.empty()) {
                            return;
                        }

                        task = std::move(this->tasks.front());
                        this->tasks.pop();
                    }

                    task();
                }
            });
        }
    }

    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args) {
        using return_type = std::invoke_result_t<F, Args...>;

        auto task = std::make_shared<std::packaged_task<return_type()>>(
            std::bind(std::forward<F>(f), std::forward<Args>(args)...)
        );

        std::future<return_type> result = task->get_future();

        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            if (stop) {
                throw std::runtime_error("enqueue on stopped ThreadPool");
            }

            tasks.emplace([task]() { (*task)(); });
        }

        condition.notify_one();
        return result;
    }

    ~ThreadPool() {
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            stop = true;
        }

        condition.notify_all();

        for (std::thread& worker : workers) {
            worker.join();
        }
    }
};

This thread pool implementation leverages RAII, move semantics, and other modern C++ features to create a clean, leak-free API.

Compiler Support and Portability

As with all new language features, compiler support for C++23 varies. Here’s a quick overview as of early 2025:

FeatureGCCClangMSVC
std::expected✅ 12.1+✅ 15+✅ 19.34+
std::format✅ 11+✅ 14+✅ 19.29+
std::mdspan✅ 12+✅ 15+✅ 19.35+
Modules⚠️ Partial⚠️ Partial✅ 19.31+
String contains/starts_with/ends_with✅ 11+✅ 13+✅ 19.26+

⚠️ Portability Tip: When working on cross-platform projects, I maintain a compatibility layer that gracefully degrades when newer features aren’t available. For example, I’ll define my own expected implementation if the standard library doesn’t provide one.

Conclusion: Embracing Modern C++

My journey with C++23 has convinced me that the language continues to evolve in the right direction. By adopting these modern features, we can write code that’s:

The best part is that we can adopt these features incrementally. You don’t need to rewrite your entire codebase—start with new components, or gradually refactor existing ones as you touch them.

What’s your experience with C++23? Have you found other features particularly useful? Let me know in the comments!


Suggest Changes

Previous Post
FastCode.Guru Website, Inside & Out
Next Post
Practical Profiling with perf on Linux