Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

README.md

C++23 Flat Map Implementation with Railway-Oriented Programming

The flat container pattern represents a fundamental paradigm shift in associative container design, moving away from traditional tree-based structures toward cache-efficient array-based implementations. This pattern emerged in the early 2000s from performance-critical domains such as game development, high-frequency trading, and embedded systems, where cache locality often trumps theoretical algorithmic complexity. Unlike std::map's red-black tree implementation, which scatters nodes throughout memory requiring expensive pointer traversals, flat containers store elements in contiguous memory using sorted vectors. This approach trades O(n) insertion complexity for dramatically improved lookup and iteration performance, often achieving 2-5x speedups in real-world scenarios. The pattern gained significant traction through Boost.Container's flat_map implementation and extensive performance studies, ultimately leading to its standardization in C++23 as part of the language's evolution toward hardware-aware computing.

This implementation addresses several critical problems in modern C++ development. First, it solves the cache locality problem inherent in tree-based containers, where each traversal step requires a potentially expensive memory access to a different cache line. Second, it eliminates the memory overhead associated with tree nodes, reducing per-element storage costs by 50-70% compared to std::map. Third, it provides predictable memory layout for better debugging and profiling, essential in performance-critical applications. Fourth, it enables efficient bulk operations through sorted range construction, allowing developers to batch insertions for optimal performance. Finally, it integrates railway-oriented programming principles through std::expected, providing composable error handling without exceptions. The pattern is particularly effective for read-heavy workloads, configuration systems, symbol tables, entity-component mappings, and any scenario where lookup frequency significantly exceeds modification frequency.

Key Features

  • Contiguous Memory Layout: All elements stored in sorted vector for optimal cache performance
  • Railway-Oriented Programming: Uses std::expected for composable error handling without exceptions
  • C++23 Standards Compliance: Leverages modern C++ features including concepts and improved type safety
  • Performance Optimized: Specialized for read-heavy workloads with superior lookup and iteration speed
  • Memory Efficient: Eliminates tree node overhead, reducing memory usage by 50-70%
  • Batch Operations: Efficient construction from sorted ranges for bulk data loading
  • STL Compatible: Drop-in replacement for std::map in many scenarios

Basic Usage

#include "flat_map.hpp"
using namespace performance_containers;

// Create flat map with capacity hint
FlatMap<int, std::string> cache{100};

// Insert elements (returns std::expected)
auto result = cache.insert(42, "answer");
if (result.has_value()) {
    LOG_INFO_PRINT("insertion successful");
} else {
    LOG_ERROR_PRINT("insertion failed: {}", error_message(result.error()));
}

// Safe lookup with error handling
auto lookup = cache.find(42);
if (lookup.has_value()) {
    LOG_INFO_PRINT("found: {}", lookup.value()->second);
}

// Range-based iteration (cache-friendly)
for (const auto& [key, value] : cache) {
    LOG_INFO_PRINT("key: {}, value: {}", key, value);
}

Advanced Usage Patterns

Bulk Loading from Sorted Data

// Efficient construction from pre-sorted data
std::vector<std::pair<int, std::string>> sorted_data{
    {1, "one"}, {2, "two"}, {3, "three"}
};

FlatMap<int, std::string> optimized{sorted_data.begin(), sorted_data.end(), true};

Railway-Oriented Error Handling

auto process_user_data(const FlatMap<int, User>& users, int user_id) 
    -> std::expected<ProcessResult, DataError> {
    
    return users.find(user_id)
        .and_then([](const auto& user_iter) -> std::expected<User, DataError> {
            return validate_user(user_iter->second);
        })
        .and_then([](const User& user) -> std::expected<ProcessResult, DataError> {
            return process_validated_user(user);
        });
}

Configuration Cache Pattern

class ConfigurationManager {
    FlatMap<std::string, std::string> m_config_cache{};
    
public:
    void load_configuration(const std::vector<ConfigEntry>& entries) {
        m_config_cache.reserve(entries.size());
        for (const auto& entry : entries) {
            m_config_cache.insert(entry.key, entry.value);
        }
    }
    
    std::expected<std::string, ConfigError> get_config(const std::string& key) const {
        return m_config_cache.at(key)
            .transform_error([](FlatMapError) { return ConfigError::KeyNotFound; });
    }
};

Performance Characteristics

Operation std::map FlatMap Notes
Lookup O(log n) O(log n) FlatMap 2-5x faster due to cache locality
Iteration O(n) O(n) FlatMap 5-10x faster, sequential memory access
Insertion O(log n) O(n) std::map faster for random insertions
Construction O(n log n) O(n²) / O(n log n)* *O(n log n) with sorted input
Memory Usage High overhead Minimal overhead 50-70% reduction in memory usage

When to Use Flat Containers

Choose FlatMap When:

  • Read-heavy workloads (lookups dominate over insertions)
  • Memory efficiency is critical (embedded systems, mobile)
  • Cache performance matters (real-time systems, games)
  • Predictable access patterns allow for batch updates
  • Small to medium datasets (<10K elements typically)

Choose std::map When:

  • Frequent insertions/deletions throughout lifecycle
  • Very large datasets where O(n) insertions become prohibitive
  • Stable iterators required (don't invalidate on modification)
  • Unpredictable access patterns with mixed operations

Real-World Examples

Game Development

// Entity-component system with fast lookups
FlatMap<EntityID, TransformComponent> transforms{10000};
FlatMap<EntityID, RenderComponent> renderables{5000};

// Game loop - frequent component access
for (const auto& [entity_id, transform] : transforms) {
    if (auto render = renderables.find(entity_id); render.has_value()) {
        update_render_system(transform, render.value()->second);
    }
}

Financial Systems

// Price lookup table with microsecond requirements
FlatMap<std::string, PriceData> price_cache{1000};

auto get_current_price(const std::string& symbol) -> std::expected<Price, PriceError> {
    return price_cache.find(symbol)
        .transform([](const auto& iter) { return iter->second.current_price; })
        .transform_error([](FlatMapError) { return PriceError::SymbolNotFound; });
}

Compiler Symbol Tables

// Variable resolution during compilation
FlatMap<std::string, SymbolInfo> symbol_table{500};

auto resolve_symbol(const std::string& name) -> std::expected<SymbolInfo, CompileError> {
    return symbol_table.at(name)
        .transform_error([&](FlatMapError) { 
            return CompileError{std::format("Undefined symbol: {}", name)}; 
        });
}

Error Handling Philosophy

This implementation embraces railway-oriented programming principles, treating errors as first-class values rather than exceptional conditions. All operations that can fail return std::expected<T, ErrorType>, enabling composable error handling through monadic operations like and_then(), transform(), and transform_error().

// Composable error handling pipeline
auto result = flat_map.find(key)
    .and_then([](const auto& iter) { return validate_data(iter->second); })
    .and_then([](const auto& data) { return process_data(data); })
    .transform_error([](auto error) { 
        LOG_ERROR_PRINT("pipeline failed: {}", error_message(error));
        return ProcessingError::ValidationFailed;
    });

Books and References

This pattern and its underlying principles are discussed in several authoritative sources:

  • "Effective STL" by Scott Meyers - Covers container selection criteria and performance considerations that led to the development of flat containers, emphasizing the importance of understanding your data access patterns.

  • "C++ High Performance" by Björn Andrist and Viktor Sehr - Dedicates significant coverage to cache-friendly data structures and memory layout optimization, providing the theoretical foundation for flat container advantages.

  • "Game Engine Architecture" by Jason Gregory - Discusses entity-component systems and performance-critical data structures used in game development, where flat containers first gained widespread adoption.

  • "Systems Performance: Enterprise and the Cloud" by Brendan Gregg - Provides detailed analysis of cache behavior and memory hierarchies that explain why contiguous data structures outperform pointer-based alternatives.

  • "The C++ Programming Language" by Bjarne Stroustrup (4th Edition) - Discusses the evolution of STL containers and the rationale behind adding flat containers to the standard library.

  • "Professional C++" by Marc Gregoire - Covers modern C++ best practices including error handling patterns and the railway-oriented programming approach demonstrated in this implementation.

  • "High-Frequency Trading: A Practical Guide to Algorithmic Strategies" by Irene Aldridge - Demonstrates the critical importance of cache-efficient data structures in latency-sensitive financial applications.

The railway-oriented programming pattern used for error handling is extensively covered in "Domain Modeling Made Functional" by Scott Wlaschin, though originally demonstrated in F#, the principles translate directly to modern C++ with std::expected.

Compilation Requirements

  • C++23 compatible compiler (GCC 13+, Clang 16+, MSVC 2022 17.6+)
  • Standard library support for std::expected and concepts
  • Optimization recommended (-O2 or -O3) for best performance results
# Compilation example
g++ -std=c++23 -O2 -Wall -Wextra main.cpp flat_map.cpp -o flat_map_demo

Testing and Validation

The implementation includes comprehensive test suites covering:

  • Correctness validation through extensive assertion-based testing
  • Performance benchmarking comparing against std::map
  • Edge case handling including empty containers and large datasets
  • Memory leak detection ensuring proper RAII compliance
  • Error path testing validating railway-oriented programming flows

Run tests with detailed logging:

./flat_map_demo 2>&1 | tee test_results.log

Migration Strategy

For existing codebases using std::map:

  1. Profile current usage to identify read-heavy containers
  2. Benchmark with test data using your specific access patterns
  3. Implement gradual migration starting with non-critical systems
  4. Monitor performance gains and adjust based on real-world usage
  5. Document performance characteristics for future container selection decisions

License

This code is provided under the MIT License. Feel free to use, modify, and distribute as needed.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.