Skip to content

[Bug Report] Market won't compile — PoolAlloc promises allocator move-propagation but deletes its move-assignment #1

Description

@OPTIONPOOL

Found while integrating wezostradingengine into an open matching-engine benchmark, the Matching Engine Performance Challenge — it cross-checks engines against the byte-identical consensus of other open-source engines. Building the matcher to drive it fails to compile in Market.cpp, where Market's assignment move-assigns its PoolAlloc-backed order maps but PoolAlloc deletes the move-assignment its own allocator traits promise. Pinned at master HEAD (1dbc25caec72e856d7f648b3710a62c2e98767e4).

Environment

  • Commit: 1dbc25caec72e856d7f648b3710a62c2e98767e4
  • Language/toolchain: C++17, g++ 14 (libstdc++)
  • Build: compiling TradingEngine/Market.cpp (e.g. g++ -std=c++17 -c TradingEngine/Market.cpp)

What happens

Compiling Market.cpp stops with use of deleted function 'PoolAlloc<...>::operator=(PoolAlloc&&)'. The order books are std::maps whose node allocator is PoolAlloc (market_helper.h: OrderMap<...> = std::map<int64_t, Orders<Order>, Sort, PoolAlloc<...>>). PoolAlloc declares propagate_on_container_move_assignment = std::true_type — a promise to the standard library that, on a container move-assignment, the allocator itself will be move-assigned — but PoolAlloc's move-assignment operator is = delete. So any move-assignment of one of those maps is ill-formed, and Market performs exactly that.

Minimal reproduction

A std::map that uses PoolAlloc, then a move-assignment — the same operation Market::operator= performs internally:

#include <map>
#include <functional>
#include "TradingEngine/PoolAlloc.h"

int main() {
    using Alloc = PoolAlloc<std::pair<const long, int>, 8>;
    std::map<long, int, std::less<long>, Alloc> a, b;
    b = std::move(a);          // <-- ill-formed
    return static_cast<int>(b.size());
}

Output (g++ 14):

.../bits/alloc_traits.h:751:15: error: use of deleted function
   'PoolAlloc<T, initialSize>& PoolAlloc<T, initialSize>::operator=(PoolAlloc&&)'
  751 |         __one = std::move(__two);
PoolAlloc.h:39:20: note: declared here
   39 |     PoolAlloc& operator=(PoolAlloc&&) = delete;

The same deleted-member pattern also blocks keeping a Market in a std::vector — which MarketManager does (std::unordered_map<int32_t, std::vector<Market>>): when the vector grows it move-constructs Market, which needs PoolAlloc's move (then copy) constructor, both = delete. Replacing std::map with std::vector<Map> in the snippet above and forcing a reallocation reproduces that second variant.

Mechanism / root cause

TradingEngine/PoolAlloc.h — the stateful block allocator for the order-map nodes:

template <typename T, size_t initialSize>
struct PoolAlloc {
    ...
    PoolAlloc(const PoolAlloc&) = delete;
    PoolAlloc& operator=(const PoolAlloc&) = delete;
    PoolAlloc(PoolAlloc&&) = delete;
    PoolAlloc& operator=(PoolAlloc&&) = delete;          // line 39

    using propagate_on_container_move_assignment = std::true_type;   // line 41
    ...
private:
    std::vector<std::vector<T>> memory;
    std::vector<T*> available;
};

propagate_on_container_move_assignment = std::true_type tells the library that a container move-assignment will move-assign the allocator (std::__alloc_on_move does dst_alloc = std::move(src_alloc)). But that operator is = delete, so the promise cannot be kept and the map's move-assignment is ill-formed. Market depends on it: Market.h:36 declares Market& operator=(Market&&) noexcept = default;, and Market.cpp implements copy-assignment in terms of it:

Market& Market::operator=(const Market& other) {
    Market market(other);       // copy-construct
    *this = std::move(market);   // <-- move-assigns the PoolAlloc-backed maps
    return *this;
}

so simply compiling Market.cpp instantiates the deleted move-assignment. (The deleted move/copy constructors surface the same way once a std::vector<Market> reallocates.)

Suggested fix

PoolAlloc's only state is two std::vectors, both copy- and move-able, so the deleted special members can be defaulted — which both honours the propagate_on_container_move_assignment promise and keeps the natural "each allocator owns its own pool" semantics:

-    PoolAlloc(const PoolAlloc&) = delete;
-    PoolAlloc& operator=(const PoolAlloc&) = delete;
-    PoolAlloc(PoolAlloc&&) = delete;
-    PoolAlloc& operator=(PoolAlloc&&) = delete;
+    PoolAlloc(const PoolAlloc&) = default;
+    PoolAlloc& operator=(const PoolAlloc&) = default;
+    PoolAlloc(PoolAlloc&&) = default;
+    PoolAlloc& operator=(PoolAlloc&&) = default;

Defaulting just the move-assignment is enough to compile Market.cpp; defaulting all four also lets Market live in MarketManager's std::vector<Market>. With these defaulted, the minimal reproduction (and the std::vector<Market> path) compile cleanly, and matching is unchanged — the change only affects how the maps copy/move their allocator, which the hot path never exercises. (Alternatively, if the intent was that these maps must never be moved, dropping propagate_on_container_move_assignment = std::true_type removes the contradiction the other way — but defaulting keeps Market's value semantics, which MarketManager and the copy-assignment operator already rely on.)

This is a time-stamped snapshot of a specific commit, offered back — not a comment on the project or the author. Thank you for sharing wezostradingengine; the simulate-then-commit Market design and the pooled std::map books were a pleasure to read and drive directly.

Respectfully submitted.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions