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.
Found while integrating
wezostradingengineinto 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 inMarket.cpp, whereMarket's assignment move-assigns itsPoolAlloc-backed order maps butPoolAllocdeletes the move-assignment its own allocator traits promise. Pinned atmasterHEAD (1dbc25caec72e856d7f648b3710a62c2e98767e4).Environment
1dbc25caec72e856d7f648b3710a62c2e98767e4TradingEngine/Market.cpp(e.g.g++ -std=c++17 -c TradingEngine/Market.cpp)What happens
Compiling
Market.cppstops withuse of deleted function 'PoolAlloc<...>::operator=(PoolAlloc&&)'. The order books arestd::maps whose node allocator isPoolAlloc(market_helper.h:OrderMap<...> = std::map<int64_t, Orders<Order>, Sort, PoolAlloc<...>>).PoolAllocdeclarespropagate_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 — butPoolAlloc's move-assignment operator is= delete. So any move-assignment of one of those maps is ill-formed, andMarketperforms exactly that.Minimal reproduction
A
std::mapthat usesPoolAlloc, then a move-assignment — the same operationMarket::operator=performs internally:Output (g++ 14):
The same deleted-member pattern also blocks keeping a
Marketin astd::vector— whichMarketManagerdoes (std::unordered_map<int32_t, std::vector<Market>>): when the vector grows it move-constructsMarket, which needsPoolAlloc's move (then copy) constructor, both= delete. Replacingstd::mapwithstd::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:propagate_on_container_move_assignment = std::true_typetells the library that a container move-assignment will move-assign the allocator (std::__alloc_on_movedoesdst_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.Marketdepends on it:Market.h:36declaresMarket& operator=(Market&&) noexcept = default;, andMarket.cppimplements copy-assignment in terms of it:so simply compiling
Market.cppinstantiates the deleted move-assignment. (The deleted move/copy constructors surface the same way once astd::vector<Market>reallocates.)Suggested fix
PoolAlloc's only state is twostd::vectors, both copy- and move-able, so the deleted special members can be defaulted — which both honours thepropagate_on_container_move_assignmentpromise and keeps the natural "each allocator owns its own pool" semantics:Defaulting just the move-assignment is enough to compile
Market.cpp; defaulting all four also letsMarketlive inMarketManager'sstd::vector<Market>. With these defaulted, the minimal reproduction (and thestd::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, droppingpropagate_on_container_move_assignment = std::true_typeremoves the contradiction the other way — but defaulting keepsMarket's value semantics, whichMarketManagerand 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-commitMarketdesign and the pooledstd::mapbooks were a pleasure to read and drive directly.Respectfully submitted.