From 792df8b297b5990b41c11830ad50812c32e20424 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 30 Nov 2025 17:02:48 -0500 Subject: [PATCH] xo-ordinaltree: custom allocator support in RB tree --- .../include/xo/ordinaltree/RedBlackTree.hpp | 222 ++++++++++++------ xo-ordinaltree/utest/redblacktree.cpp | 2 +- 2 files changed, 156 insertions(+), 68 deletions(-) diff --git a/xo-ordinaltree/include/xo/ordinaltree/RedBlackTree.hpp b/xo-ordinaltree/include/xo/ordinaltree/RedBlackTree.hpp index 337a78cb..9c859d54 100644 --- a/xo-ordinaltree/include/xo/ordinaltree/RedBlackTree.hpp +++ b/xo-ordinaltree/include/xo/ordinaltree/RedBlackTree.hpp @@ -72,7 +72,10 @@ namespace xo { * - Reduce.operator() :: (Accumulator x Key) -> Accumulator * - Reduce.operator() :: (Accumulator x Accumulator) -> Accumulator */ - template > + template , + typename Allocator = std::allocator>> class RedBlackTree; namespace detail { @@ -114,10 +117,24 @@ namespace xo { contents_{std::move(kv_pair)}, reduced_{std::move(r)} {} - static Node * make_leaf(value_type const & kv_pair, + template + static Node * make_leaf(NodeAllocator& alloc, + value_type const & kv_pair, ReducedValue const & leaf_rv) { - return new Node(kv_pair, - std::pair(leaf_rv, leaf_rv)); + using traits = std::allocator_traits; + + // get memory + Node * node = traits::allocate(alloc, 1); + try { + // placemenent new + traits::construct(alloc, node, kv_pair, + std::pair(leaf_rv, leaf_rv)); + return node; + } catch(...) { + traits::deallocate(alloc, node, 1); + throw; + + } } /*make_leaf*/ static Node * make_leaf(value_type && kv_pair, @@ -260,7 +277,7 @@ namespace xo { size_t size() const { return size_; } /* const access */ - ContentsType const & contents() const { return contents_; } + value_type const & contents() const { return contents_; } /* non-const value access. * * editorial: would prefer to return @@ -270,7 +287,7 @@ namespace xo { * is considered unrelated to std::pair, * so l-value conversion not allowed */ - ContentsType & contents() { return contents_; } + value_type & contents() { return contents_; } Node * parent() const { return parent_; } Node * child(Direction d) const { return child_v_[d]; } @@ -403,7 +420,7 @@ namespace xo { * .second = value associated with this node * .third = reduced value */ - ContentsType contents_; + value_type contents_; /* accumulator for some binary function of Values. * must be associative, since value will be produced * by any ordering of calls to Reduce::combine(). @@ -1230,8 +1247,10 @@ namespace xo { * - f=false for existing node (k already in tree before this call) * - n=node containing key k */ + template static std::pair - insert_aux(value_type const & kv_pair, + insert_aux(NodeAllocator & alloc, + value_type const & kv_pair, bool allow_replace_flag, Reduce const & reduce_fn, RbNode ** pp_root) @@ -1281,7 +1300,9 @@ namespace xo { /* invariant: N->child(d) is nil */ if (N) { - RbNode * new_node = RbNode::make_leaf(kv_pair, + RbNode * new_node = RbNode::make_leaf(alloc, + kv_pair, + reduce_fn.leaf(kv_pair.second)); N->assign_child_reparent(d, new_node); @@ -1300,7 +1321,8 @@ namespace xo { */ return std::make_pair(true, new_node); } else { - *pp_root = RbNode::make_leaf(kv_pair, + *pp_root = RbNode::make_leaf(alloc, + kv_pair, reduce_fn.leaf(kv_pair.second)); /* tree with a single node might as well be black */ @@ -1331,7 +1353,9 @@ namespace xo { * - N has no child nodes * - N->parent() != nullptr */ - static void remove_black_leaf(RbNode *N, + template + static void remove_black_leaf(NodeAllocator & alloc, + RbNode *N, Reduce const & reduce_fn, bool debug_flag, RbNode **pp_root) @@ -1340,6 +1364,8 @@ namespace xo { using xo::xtag; using xo::print::ccs; + using traits = std::allocator_traits; + //constexpr char const *c_self = "RbTreeUtil::remove_black_leaf"; scope log(XO_DEBUG(debug_flag)); @@ -1351,7 +1377,7 @@ namespace xo { if (!P) { /* N was the root node, tree now empty */ *pp_root = nullptr; - delete N; + traits::deallocate(alloc, N, 1); return; } @@ -1360,7 +1386,7 @@ namespace xo { */ Direction d = P->replace_child_reparent(N, nullptr); - delete N; + traits::deallocate(alloc, N, 1); /* need to delay this assignment until * we've determined d @@ -1784,16 +1810,18 @@ namespace xo { * * return true if a node was removed; false otherwise. */ - static bool erase_aux(Key const &k, + template + static bool erase_aux(NodeAllocator & alloc, + Key const & k, Reduce const & reduce_fn, bool debug_flag, - RbNode **pp_root) { + RbNode ** pp_root) { using xo::scope; using xo::xtag; scope log(XO_DEBUG(debug_flag)); - RbNode *N = *pp_root; + RbNode * N = *pp_root; log && log("enter", xtag("root", N)); @@ -1843,7 +1871,7 @@ namespace xo { if (X == nullptr) { /* N has 0 or 1 children */ - erase_1child_aux(N, reduce_fn, debug_flag, pp_root); + erase_1child_aux(alloc, N, reduce_fn, debug_flag, pp_root); } else { /* R->right_child() is nil by definition */ @@ -1934,7 +1962,7 @@ namespace xo { RbTreeUtil::display_aux(D_Invalid, R, 0 /*depth*/, &log); } - erase_1child_aux(N, reduce_fn, debug_flag, pp_root); + erase_1child_aux(alloc, N, reduce_fn, debug_flag, pp_root); } else { /* * here the triangle ascii art indicates a tree structure, @@ -1954,7 +1982,8 @@ namespace xo { */ /* will be swapping info in {R, N}: - * everything except RbNode.contents_ + * everything except RbNode.contents_. + * Annoying but necessary to have stable Node memory locations */ RbNode::swap_locations(R, N, debug_flag); @@ -1978,48 +2007,34 @@ namespace xo { * / . * W */ - erase_1child_aux(N, reduce_fn, debug_flag, pp_root); + erase_1child_aux(alloc, N, reduce_fn, debug_flag, pp_root); } -#ifdef GOING_AWAY +#ifdef OBSOLETE /* would be convenient to just make this assignment, * but several disadvantages: * 1. invalidates an iterator pointing to R * when nearby-in-key-space N gets deleted * 2. gives up key constness */ - N->contents_ = R->contents_; - /* (preserving - * N->color_, - * N->size_, - * N->parent_, - * N->reduced_, - * N->child_v_[]) - */ - - /* now relabel N as new R (R'), - * and relabel R as new N (N'). - * Then go to work on reduced problem of deleting N'. - * Problem is redueced since now N' has 0 or 1 child. - * - * (Doesn't matter that N' contains key,values of R, - * since we're going to delete it anyway) - */ - N = R; - /* (preserving R->parent_, R->child_v_[]) */ + N->contents_ = R->contents_; N = R; #endif } return true; } /*erase_aux*/ - static void erase_1child_aux(RbNode * N, + template + static void erase_1child_aux(NodeAllocator & alloc, + RbNode * N, Reduce const & reduce_fn, bool debug_flag, RbNode ** pp_root) { scope log(XO_DEBUG(debug_flag)); + using traits = std::allocator_traits; + RbNode * P = N->parent(); /* N has 0 or 1 children @@ -2054,7 +2069,7 @@ namespace xo { } log && log("delete red root node", xtag("addr", N)); - delete N; + traits::deallocate(alloc, N, 1); } else { assert(false); @@ -2088,24 +2103,24 @@ namespace xo { } log && log("delete node", xtag("addr", N)); - delete N; + traits::deallocate(alloc, N, 1); } else { /* N is black with no children, * may need rebalance here */ if (P) { - RbTreeUtil::remove_black_leaf(N, reduce_fn, debug_flag, pp_root); + RbTreeUtil::remove_black_leaf(alloc, N, reduce_fn, debug_flag, pp_root); } else { /* N was root node */ *pp_root = nullptr; log && log("delete black root node", xtag("addr", N)); - delete N; + traits::deallocate(alloc, N, 1); } } } - } + } /*erase_1child_aux*/ /* verify that subtree at N is in RB-shape. * will cover subset of RedBlackTree class invariants: @@ -2120,6 +2135,7 @@ namespace xo { * f(f(L, Node::value), R) * where: L is reduced-value for left child, * R is reduced-value for right child + * RB8. inorder traversal visits all the keys in subtree * * returns the #of nodes in subtree rooted at N. */ @@ -2271,6 +2287,14 @@ namespace xo { if (p_black_height) *p_black_height = black_height; + /* RB8. inorder traversal visits all the nodes */ + std::size_t subtree_z = N ? N->size() : 0ul; + + XO_EXPECT(i_node == subtree_z, + tostr(c_self, ": expect visit count = node.size", + xtag("visit_count", i_node), + xtag("node.size", 0))); + return i_node; } /*verify_subtree_ok*/ @@ -2477,7 +2501,7 @@ namespace xo { struct NodeTypeTraits { using NativeNodeType = Node; using NodeType = NativeNodeType; - using ContentsType = typename NodeType::ContentsType; + using ContentsType = typename NodeType::value_type; using NodePtrType = NodeType *; }; @@ -2487,7 +2511,7 @@ namespace xo { struct NodeTypeTraits { using NativeNodeType = Node; using NodeType = NativeNodeType const; - using ContentsType = typename NodeType::ContentsType const; + using ContentsType = typename NodeType::value_type const; using NodePtrType = NodeType const *; }; @@ -2905,9 +2929,29 @@ namespace xo { bool is_equal(value_type const & x, value_type const & y) const { return x == y; } }; /*SumReduce*/ - /* red-black tree with order statistics - */ - template + /** @class RedBlackTree + * @brief red-black tree with order statistics + * + * Lazily balanced. Longest path to a leaf is at most 2x the length of shortest path. + * + * Maintains order statistics. Accumulates some associative relation on key,value pairs. + * + * Can obtain iterator to k'th element of a tree with n nodes in log(n) time. + * Allows behaving as a weak random-access iterator with log(n) cost per query + * + * Missing Features: + * 1. efficient iterator arithmetic + * 2. pretty printing + * 3. reflection support + * 4. custom allocation support [WIP] + * 5. custom key compare + * 6. garbage collector integration + * 7. std library integration + **/ + template class RedBlackTree { static_assert(ReduceConcept); //static_assert(requires(Reduce r) { r.nil(); }, "missing .nil() method"); @@ -2916,11 +2960,20 @@ namespace xo { using key_type = Key; using mapped_type = Value; using value_type = std::pair; + // using key_compare = Compare // not yet + using allocator_type = Allocator; + using allocator_traits = std::allocator_traits; + using ReducedValue = typename Reduce::value_type; using RbTreeLhs = detail::RedBlackTreeLhs>; using RbTreeConstLhs = detail::RedBlackTreeConstLhs>; using RbUtil = detail::RbTreeUtil; using RbNode = detail::Node; + + using node_type = RbNode; + using node_allocator_type = typename std::allocator_traits::template rebind_alloc; + using node_allocator_traits = std::allocator_traits; + using Direction = detail::Direction; using size_type = std::size_t; using difference_type = std::ptrdiff_t; @@ -2928,7 +2981,32 @@ namespace xo { using const_iterator = detail::ConstIterator; public: - explicit RedBlackTree(bool debug_flag = false) : debug_flag_{debug_flag} {} + explicit RedBlackTree(const allocator_type & alloc = allocator_type{}, + bool debug_flag = false) : + node_alloc_{alloc}, + debug_flag_{debug_flag} {} +#ifdef NOT_YET + RedBlackTree(const RedBlackTree & other) : + node_alloc_{node_allocator_traits::select_on_container_copy_construction(other.node_alloc_)} + //, compare_{other.compare_} + { + copy_from(other); + } + + // similar: copy constructor with explicit alloc + RedBlackTree(const RedBlackTree & other, const allocator_type & alloc) : + node_alloc_{alloc} + { + copy_from(other); + } + + // move ctor + RedBlackTree(RedBlackTree && other) noexcept : + node_alloc_{std::move(other.node_alloc_)}, + root_{other.root_}, size_{other.size} + // , compare_{other.compare_)} + {} +#endif bool empty() const { return size_ == 0; } size_type size() const { return size_; } @@ -3145,10 +3223,11 @@ namespace xo { */ RbTreeLhs operator[](Key const & k) { std::pair insert_result - = RbUtil::insert_aux(value_type(k, Value() /*used iff creating new node*/), - false /*allow_replace_flag*/, - this->reduce_fn_, - &(this->root_)); + = RbUtil::template insert_aux(this->node_alloc_, + value_type(k, Value() /*used iff creating new node*/), + false /*allow_replace_flag*/, + this->reduce_fn_, + &(this->root_)); return RbTreeLhs(this, insert_result.second, k); } /*operator[]*/ @@ -3260,7 +3339,8 @@ namespace xo { scope log(XO_DEBUG(c_logging_enabled)); std::pair insert_result - = RbUtil::insert_aux(std::move(kv_pair), + = RbUtil::insert_aux(this->node_alloc_, + std::move(kv_pair), true /*allow_replace_flag*/, this->reduce_fn_, &(this->root_)); @@ -3275,13 +3355,14 @@ namespace xo { insert_result.first)); } /*insert*/ - bool erase(Key const & k) { + bool erase(Key const & key) { scope log(XO_DEBUG(debug_flag_), xtag("size", size_)); if (log) { - log("pre", xtag("tree", *this)); + log("pre", xtag("key", key), xtag("tree", *this)); } - bool retval = RbUtil::erase_aux(k, + bool retval = RbUtil::erase_aux(this->node_alloc_, + key, this->reduce_fn_, debug_flag_, &(this->root_)); @@ -3321,9 +3402,8 @@ namespace xo { using xo::xtag; constexpr const char *c_self = "RedBlackTree::verify_ok"; - constexpr bool c_logging_enabled = false; - scope log(XO_DEBUG(c_logging_enabled)); + scope log(XO_DEBUG(debug_flag_)); /* RB0. */ if (root_ == nullptr) { @@ -3356,7 +3436,7 @@ namespace xo { xtag("self.size", size_), xtag("n", n_node))); - if (c_logging_enabled) + if (debug_flag_) log && log(xtag("size", this->size_), xtag("blackheight", black_height)); @@ -3366,13 +3446,21 @@ namespace xo { void display() const { RbUtil::display(this->root_, 0); } /*display*/ private: - /* #of key/value pairs in this tree */ + + + private: + /** allocator state **/ + node_allocator_type node_alloc_; + /** number of nodes in this tree. Each node holds one (key,value) pair **/ size_t size_ = 0; - /* root of red/black tree */ + /** root of red/black tree. Empty tree has null root. **/ RbNode * root_ = nullptr; - /* .reduce_fn :: (Accumulator x Key) -> Accumulator */ + /** accumulates custom order statistics; + * for example partial sums of @tparam Values + * reduce_fn_:: (Accumulator x Key) -> Accumulator + **/ Reduce reduce_fn_; - /* true to enable debug logging */ + /** true to enable debug logging **/ bool debug_flag_ = false; }; /*RedBlackTree*/ diff --git a/xo-ordinaltree/utest/redblacktree.cpp b/xo-ordinaltree/utest/redblacktree.cpp index 669f3d26..75eb6c83 100644 --- a/xo-ordinaltree/utest/redblacktree.cpp +++ b/xo-ordinaltree/utest/redblacktree.cpp @@ -154,7 +154,7 @@ namespace { TEST_CASE("rbtree", "[redblacktree]") { constexpr bool c_debug_flag = false; - RbTree rbtree{c_debug_flag}; + RbTree rbtree{RbTree::allocator_type{}, c_debug_flag}; std::uint64_t seed = 14950349842636922572UL; /* can reseed from /dev/urandom with: */