From e48c349f6332135f25b1fdb2250250ed644f12a5 Mon Sep 17 00:00:00 2001 From: Sergei Date: Fri, 9 Aug 2024 14:48:34 +0300 Subject: [PATCH] Support moving nodes and refactor to eliminate recursion (#14) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor to eliminate recursion * micro fix * Added new `postOrderTraverse` method. * style fix * docs * minor sonar fixes * Make nodes movable; Rename `traverse` → `traverseInOrder`. Added new `traversePostOrder`. * fix style * minor fix * Fixed tree movement - still constant time! (#15) * - Return back to 4x ptr node footprint - Introduced new "origin" fake node at tree. - Fixed tree movement - still constant time! * minor fix * delete not in use constructor * sonar fix --- c++/cavl.hpp | 476 +++++++++++++++++++++++++++++++++++++-------------- c++/test.cpp | 211 +++++++++++++++++++++-- 2 files changed, 539 insertions(+), 148 deletions(-) diff --git a/c++/cavl.hpp b/c++/cavl.hpp index 42ef4b6..2516d71 100644 --- a/c++/cavl.hpp +++ b/c++/cavl.hpp @@ -29,6 +29,7 @@ #include #include #include +#include /// If CAVL is used in throughput-critical code, then it is recommended to disable assertion checks as they may /// be costly in terms of execution time. @@ -55,8 +56,12 @@ class Tree; /// The worst-case complexity of all operations is O(log n), unless specifically noted otherwise. /// Note that this class has no public members. The user type should re-export them if needed (usually it is not). /// The size of this type is 4x pointer size (16 bytes on a 32-bit platform). +/// +/// No Sonar cpp:S1448 b/c this is the main node entity without public members - maintainability is not a concern here. +/// No Sonar cpp:S4963 b/c `Node` supports move operation. +/// template -class Node +class Node // NOSONAR cpp:S1448 cpp:S4963 { // Polyfill for C++17's std::invoke_result_t. template @@ -76,19 +81,24 @@ class Node Node(const Node&) = delete; auto operator=(const Node&) -> Node& = delete; - // They can't be moved either, but the reason is less obvious. - // While we can trivially update the pointers in the adjacent nodes to keep the tree valid, - // we can't update external references to the tree. This breaks the tree if one attempted to move its root node. - Node(Node&& other) = delete; - auto operator=(Node&& other) -> Node& = delete; + // Tree nodes can be moved. We update the pointers in the adjacent nodes to keep the tree valid, + // as well as root node pointer if needed (see `moveFrom`). This operation is constant time. + Node(Node&& other) noexcept { moveFrom(other); } + auto operator=(Node&& other) noexcept -> Node& + { + moveFrom(other); + return *this; + } protected: Node() = default; ~Node() = default; /// Accessors for advanced tree introspection. Not needed for typical usage. - auto getParentNode() noexcept -> Derived* { return down(up); } - auto getParentNode() const noexcept -> const Derived* { return down(up); } + bool isLinked() const noexcept { return nullptr != up; } + bool isRoot() const noexcept { return isLinked() && (!up->isLinked()); } + auto getParentNode() noexcept -> Derived* { return isRoot() ? nullptr : down(up); } + auto getParentNode() const noexcept -> const Derived* { return isRoot() ? nullptr : down(up); } auto getChildNode(const bool right) noexcept -> Derived* { return down(lr[right]); } auto getChildNode(const bool right) const noexcept -> const Derived* { return down(lr[right]); } auto getBalanceFactor() const noexcept { return bf; } @@ -100,55 +110,39 @@ class Node template static auto search(Node* const root, const Pre& predicate) noexcept -> Derived* { - Derived* p = down(root); - std::tuple const out = search
(p, predicate, []() -> Derived* { return nullptr; });
-        CAVL_ASSERT(p == root);
-        return std::get<0>(out);
+        return searchImpl(root, predicate);
     }
-
-    /// Same but const.
     template 
     static auto search(const Node* const root, const Pre& predicate) noexcept -> const Derived*
     {
-        const Node* out = nullptr;
-        const Node* n   = root;
-        while (n != nullptr)
-        {
-            const auto cmp = predicate(*down(n));
-            if (0 == cmp)
-            {
-                out = n;
-                break;
-            }
-            n = n->lr[cmp > 0];
-        }
-        return down(out);
+        return searchImpl(root, predicate);
     }
 
     /// This is like the regular search function except that if the node is missing, the factory will be invoked
     /// (without arguments) to construct a new one and insert it into the tree immediately.
-    /// The root node may be replaced in the process. If this method returns true, the tree is not modified;
+    /// The root node (inside the origin) may be replaced in the process.
+    /// If this method returns true, the tree is not modified;
     /// otherwise, the factory was (successfully!) invoked and a new node has been inserted into the tree.
     /// The factory does not need to be noexcept (may throw). It may also return nullptr to indicate intentional
     /// refusal to modify the tree, or f.e. in case of out of memory - result will be `(nullptr, true)` tuple.
     template 
-    static auto search(Derived*& root, const Pre& predicate, const Fac& factory) -> std::tuple;
+    static auto search(Node& origin, const Pre& predicate, const Fac& factory) -> std::tuple;
 
-    /// Remove the specified node from its tree. The root node may be replaced in the process.
+    /// Remove the specified node from its tree. The root node (inside the origin) may be replaced in the process.
     /// The function has no effect if the node pointer is nullptr.
     /// If the node is not in the tree, the behavior is undefined; it may create cycles in the tree which is deadly.
     /// It is safe to pass the result of search() directly as the second argument:
     ///     Node::remove(root, Node::search(root, search_predicate));
     ///
     /// No Sonar cpp:S6936 b/c the `remove` method name isolated inside `Node` type (doesn't conflict with C).
-    static void remove(Derived*& root, const Node* const node) noexcept;  // NOSONAR cpp:S6936
+    static void remove(Node& origin, const Node* const node) noexcept;  // NOSONAR cpp:S6936
 
     /// This is like the const overload of remove() except that the node pointers are invalidated afterward for safety.
     ///
     /// No Sonar cpp:S6936 b/c the `remove` method name isolated inside `Node` type (doesn't conflict with C).
-    static void remove(Derived*& root, Node* const node) noexcept  // NOSONAR cpp:S6936
+    static void remove(Node& origin, Node* const node) noexcept  // NOSONAR cpp:S6936
     {
-        remove(root, static_cast(node));
+        remove(origin, static_cast(node));
         if (nullptr != node)
         {
             node->unlink();
@@ -162,89 +156,95 @@ class Node
     static auto min(const Node* const root) noexcept -> const Derived* { return extremum(root, false); }
     static auto max(const Node* const root) noexcept -> const Derived* { return extremum(root, true); }
 
-    // NOLINTBEGIN(misc-no-recursion)
-
     /// In-order or reverse-in-order traversal of the tree; the visitor is invoked with a reference to each node.
-    /// Required stack depth is bounded by less than 2*log2(size),
-    /// hence no Sonar cpp:S925 and `NOLINT(misc-no-recursion)` exceptions.
     /// If the return type is non-void, then it shall be default-constructable and convertible to bool; in this case,
     /// traversal will stop when the first true value is returned, which is propagated back to the caller; if none
     /// of the calls returned true or the tree is empty, a default value is constructed and returned.
     /// The tree shall not be modified while traversal is in progress, otherwise bad memory access will likely occur.
     template >
-    static auto traverse(Derived* const root, const Vis& visitor, const bool reverse = false)  //
+    static auto traverseInOrder(Derived* const root, const Vis& visitor, const bool reverse = false)  //
         -> std::enable_if_t::value, R>
     {
-        if (Node* const n = root)
-        {
-            // NOLINTNEXTLINE(*-qualified-auto)
-            if (auto t = Node::traverse(down(n->lr[reverse]), visitor, reverse))  // NOSONAR cpp:S925
-            {
-                return t;
-            }
-            if (auto t = visitor(*root))  // NOLINT(*-qualified-auto)
-            {
-                return t;
-            }
-            return Node::traverse(down(n->lr[!reverse]), visitor, reverse);  // NOSONAR cpp:S925
-        }
-        return R{};
+        return traverseInOrderImpl(root, visitor, reverse);
     }
     template 
-    static auto traverse(Derived* const root, const Vis& visitor, const bool reverse = false)
+    static auto traverseInOrder(Derived* const root, const Vis& visitor, const bool reverse = false)  //
         -> std::enable_if_t>::value>
     {
-        if (Node* const n = root)
-        {
-            Node::traverse(down(n->lr[reverse]), visitor, reverse);  // NOSONAR cpp:S925
-            visitor(*root);
-            Node::traverse(down(n->lr[!reverse]), visitor, reverse);  // NOSONAR cpp:S925
-        }
+        traverseInOrderImpl(root, visitor, reverse);
     }
     template >
-    static auto traverse(const Derived* const root, const Vis& visitor, const bool reverse = false)  //
+    static auto traverseInOrder(const Derived* const root, const Vis& visitor, const bool reverse = false)  //
         -> std::enable_if_t::value, R>
     {
-        if (const Node* const n = root)
-        {
-            // NOLINTNEXTLINE(*-qualified-auto)
-            if (auto t = Node::traverse(down(n->lr[reverse]), visitor, reverse))  // NOSONAR cpp:S925
-            {
-                return t;
-            }
-            if (auto t = visitor(*root))  // NOLINT(*-qualified-auto)
-            {
-                return t;
-            }
-            return Node::traverse(down(n->lr[!reverse]), visitor, reverse);
-        }
-        return R{};
+        return traverseInOrderImpl(root, visitor, reverse);
     }
     template 
-    static auto traverse(const Derived* const root, const Vis& visitor, const bool reverse = false)
+    static auto traverseInOrder(const Derived* const root, const Vis& visitor, const bool reverse = false)  //
         -> std::enable_if_t>::value>
     {
-        if (const Node* const n = root)
+        traverseInOrderImpl(root, visitor, reverse);
+    }
+
+    /// @breaf Post-order (or reverse-post-order) traversal of the tree.
+    ///
+    /// "Post" nature of the traversal guarantees that, once a node reference is passed to the visitor,
+    /// traversal won't use or reference this node anymore, so it is safe to modify the node in the visitor -
+    /// f.e. deallocate node's memory for an efficient "release whole tree" scenario. But the tree itself
+    /// shall not be modified while traversal is in progress, otherwise bad memory access will likely occur.
+    ///
+    /// @param root The root node of the tree to traverse.
+    /// @param visitor The callable object to invoke for each node. The visitor is invoked with a reference
+    ///                to each node as a POST-action call, AFTER visiting all of its children.
+    /// @param reverse If `false`, the traversal visits first "left" children, then "right" children.
+    ///                If `true`, the traversal is performed in reverse order ("right" first, then "left").
+    ///                In either case, the current node is visited last (hence the post-order).
+    ///
+    template 
+    static void traversePostOrder(Derived* const root, const Vis& visitor, const bool reverse = false)
+    {
+        traversePostOrderImpl(root, visitor, reverse);
+    }
+    template 
+    static void traversePostOrder(const Derived* const root, const Vis& visitor, const bool reverse = false)
+    {
+        traversePostOrderImpl(root, visitor, reverse);
+    }
+
+private:
+    void moveFrom(Node& other) noexcept
+    {
+        CAVL_ASSERT(!isLinked());  // Should not be part of any tree yet.
+
+        up    = other.up;
+        lr[0] = other.lr[0];
+        lr[1] = other.lr[1];
+        bf    = other.bf;
+        other.unlink();
+
+        if (nullptr != up)
         {
-            Node::traverse(down(n->lr[reverse]), visitor, reverse);  // NOSONAR cpp:S925
-            visitor(*root);
-            Node::traverse(down(n->lr[!reverse]), visitor, reverse);  // NOSONAR cpp:S925
+            up->lr[up->lr[1] == &other] = this;
+        }
+        if (nullptr != lr[0])
+        {
+            lr[0]->up = this;
+        }
+        if (nullptr != lr[1])
+        {
+            lr[1]->up = this;
         }
     }
-    // NOLINTEND(misc-no-recursion)
 
-private:
     void rotate(const bool r) noexcept
     {
+        CAVL_ASSERT(isLinked());
         CAVL_ASSERT((lr[!r] != nullptr) && ((bf >= -1) && (bf <= +1)));
-        Node* const z = lr[!r];
-        if (up != nullptr)
-        {
-            up->lr[up->lr[1] == this] = z;
-        }
-        z->up  = up;
-        up     = z;
-        lr[!r] = z->lr[r];
+        Node* const z             = lr[!r];
+        up->lr[up->lr[1] == this] = z;
+        z->up                     = up;
+        up                        = z;
+        lr[!r]                    = z->lr[r];
         if (lr[!r] != nullptr)
         {
             lr[!r]->up = this;
@@ -256,6 +256,33 @@ class Node
 
     auto retraceOnGrowth() noexcept -> Node*;
 
+    template 
+    static void traverseInOrderImpl(DerivedT* const root, const Vis& visitor, const bool reverse);
+    template 
+    static auto traverseInOrderImpl(DerivedT* const root, const Vis& visitor, const bool reverse) -> Result;
+
+    template 
+    static void traversePostOrderImpl(DerivedT* const root, const Vis& visitor, const bool reverse);
+
+    template 
+    static auto searchImpl(NodeT* const root, const Pre& predicate) noexcept -> DerivedT*
+    {
+        NodeT* n = root;
+        while (n != nullptr)
+        {
+            CAVL_ASSERT(nullptr != n->up);
+
+            DerivedT* const derived = down(n);
+            const auto      cmp     = predicate(*derived);
+            if (0 == cmp)
+            {
+                return derived;
+            }
+            n = n->lr[cmp > 0];
+        }
+        return nullptr;
+    }
+
     void unlink() noexcept
     {
         up    = nullptr;
@@ -293,7 +320,6 @@ class Node
 
     friend class Tree;
 
-    // The binary layout is compatible with the C version.
     Node*                up = nullptr;
     std::array lr{};
     std::int8_t          bf = 0;
@@ -301,15 +327,20 @@ class Node
 
 template 
 template 
-auto Node::search(Derived*& root, const Pre& predicate, const Fac& factory) -> std::tuple
+auto Node::search(Node& origin, const Pre& predicate, const Fac& factory) -> std::tuple
 {
+    CAVL_ASSERT(!origin.isLinked());
+    Node*& root = origin.lr[0];
+
     Node* out = nullptr;
     Node* up  = root;
     Node* n   = root;
     bool  r   = false;
     while (n != nullptr)
     {
-        const auto cmp = predicate(static_cast(*n));
+        CAVL_ASSERT(n->isLinked());
+
+        const auto cmp = predicate(*down(n));
         if (0 == cmp)
         {
             out = n;
@@ -326,25 +357,27 @@ auto Node::search(Derived*& root, const Pre& predicate, const Fac& fact
     }
 
     out = factory();
+    CAVL_ASSERT(out != &origin);
     if (nullptr == out)
     {
         return std::make_tuple(nullptr, true);
     }
+    out->unlink();
 
     if (up != nullptr)
     {
         CAVL_ASSERT(up->lr[r] == nullptr);
         up->lr[r] = out;
+        out->up   = up;
     }
     else
     {
-        root = down(out);
+        root    = out;
+        out->up = &origin;
     }
-    out->unlink();
-    out->up = up;
     if (Node* const rt = out->retraceOnGrowth())
     {
-        root = down(rt);
+        root = rt;
     }
     return std::make_tuple(down(out), false);
 }
@@ -352,12 +385,16 @@ auto Node::search(Derived*& root, const Pre& predicate, const Fac& fact
 // No Sonar cpp:S6936 b/c the `remove` method name isolated inside `Node` type (doesn't conflict with C).
 // No Sonar cpp:S3776 cpp:S134 cpp:S5311 b/c this is the main removal tool - maintainability is not a concern here.
 template 
-void Node::remove(Derived*& root, const Node* const node) noexcept  // NOSONAR cpp:S6936 cpp:S3776
+void Node::remove(Node& origin, const Node* const node) noexcept  // NOSONAR cpp:S6936 cpp:S3776
 {
+    CAVL_ASSERT(!origin.isLinked());
+    CAVL_ASSERT(node != &origin);  // The origin node is not part of the tree, so it cannot be removed.
+
     if (node != nullptr)
     {
+        Node*& root = origin.lr[0];
         CAVL_ASSERT(root != nullptr);  // Otherwise, the node would have to be nullptr.
-        CAVL_ASSERT((node->up != nullptr) || (node == root));
+        CAVL_ASSERT(node->isLinked());
         Node* p = nullptr;  // The lowest parent node that suffered a shortening of its subtree.
         bool  r = false;    // Which side of the above was shortened.
         // The first step is to update the topology and remember the node where to start the retracing from later.
@@ -371,7 +408,7 @@ void Node::remove(Derived*& root, const Node* const node) noexcept  //
             re->lr[0]->up = re;
             if (re->up != node)
             {
-                p = re->up;  // Retracing starts with the ex-parent of our replacement node.
+                p = re->getParentNode();  // Retracing starts with the ex-parent of our replacement node.
                 CAVL_ASSERT(p->lr[0] == re);
                 p->lr[0] = re->lr[1];     // Reducing the height of the left subtree here.
                 if (p->lr[0] != nullptr)  // NOSONAR cpp:S134
@@ -388,13 +425,13 @@ void Node::remove(Derived*& root, const Node* const node) noexcept  //
                 r = true;  // The right child of the replacement node remains the same, so we don't bother relinking it.
             }
             re->up = node->up;
-            if (re->up != nullptr)
+            if (!re->isRoot())
             {
                 re->up->lr[re->up->lr[1] == node] = re;  // Replace link in the parent of node.
             }
             else
             {
-                root = down(re);
+                root = re;
             }
         }
         else  // Either or both of the children are nullptr.
@@ -405,7 +442,7 @@ void Node::remove(Derived*& root, const Node* const node) noexcept  //
             {
                 node->lr[rr]->up = p;
             }
-            if (p != nullptr)
+            if (!node->isRoot())
             {
                 r        = p->lr[1] == node;
                 p->lr[r] = node->lr[rr];
@@ -416,20 +453,20 @@ void Node::remove(Derived*& root, const Node* const node) noexcept  //
             }
             else
             {
-                root = down(node->lr[rr]);
+                root = node->lr[rr];
             }
         }
         // Now that the topology is updated, perform the retracing to restore balance. We climb up adjusting the
         // balance factors until we reach the root or a parent whose balance factor becomes plus/minus one, which
         // means that that parent was able to absorb the balance delta; in other words, the height of the outer
         // subtree is unchanged, so upper balance factors shall be kept unchanged.
-        if (p != nullptr)
+        if (p != &origin)
         {
             Node* c = nullptr;
             for (;;)  // NOSONAR cpp:S5311
             {
                 c = p->adjustBalance(!r);
-                p = c->up;
+                p = c->getParentNode();
                 if ((c->bf != 0) || (nullptr == p))  // NOSONAR cpp:S134
                 {
                     // Reached the root or the height difference is absorbed by `c`.
@@ -440,7 +477,7 @@ void Node::remove(Derived*& root, const Node* const node) noexcept  //
             if (nullptr == p)
             {
                 CAVL_ASSERT(c != nullptr);
-                root = down(c);
+                root = c;
             }
         }
     }
@@ -449,6 +486,7 @@ void Node::remove(Derived*& root, const Node* const node) noexcept  //
 template 
 auto Node::adjustBalance(const bool increment) noexcept -> Node*
 {
+    CAVL_ASSERT(isLinked());
     CAVL_ASSERT(((bf >= -1) && (bf <= +1)));
     Node*      out    = this;
     const auto new_bf = static_cast(bf + (increment ? +1 : -1));
@@ -510,14 +548,14 @@ template 
 auto Node::retraceOnGrowth() noexcept -> Node*
 {
     CAVL_ASSERT(0 == bf);
-    Node* c = this;      // Child
-    Node* p = this->up;  // Parent
+    Node* c = this;                   // Child
+    Node* p = this->getParentNode();  // Parent
     while (p != nullptr)
     {
         const bool r = p->lr[1] == c;  // c is the right child of parent
         CAVL_ASSERT(p->lr[r] == c);
         c = p->adjustBalance(r);
-        p = c->up;
+        p = c->getParentNode();
         if (0 == c->bf)
         {           // The height change of the subtree made this parent perfectly balanced (as all things should be),
             break;  // hence, the height of the outer subtree is unchanged, so upper balance factors are unchanged.
@@ -527,6 +565,159 @@ auto Node::retraceOnGrowth() noexcept -> Node*
     return (nullptr == p) ? c : nullptr;  // New root or nothing.
 }
 
+// No Sonar cpp:S134 b/c this is the main in-order traversal tool - maintainability is not a concern here.
+template 
+template 
+void Node::traverseInOrderImpl(DerivedT* const root, const Vis& visitor, const bool reverse)
+{
+    NodeT* node = root;
+    NodeT* prev = nullptr;
+
+    while (nullptr != node)
+    {
+        NodeT* next = node->getParentNode();
+
+        // Did we come down to this node from `prev`?
+        if (prev == next)
+        {
+            if (auto* const left = node->lr[reverse])
+            {
+                next = left;
+            }
+            else
+            {
+                visitor(*down(node));
+
+                if (auto* const right = node->lr[!reverse])  // NOSONAR cpp:S134
+                {
+                    next = right;
+                }
+            }
+        }
+        // Did we come up to this node from the left child?
+        else if (prev == node->lr[reverse])
+        {
+            visitor(*down(node));
+
+            if (auto* const right = node->lr[!reverse])
+            {
+                next = right;
+            }
+        }
+        else
+        {
+            // next has already been set to the parent node.
+        }
+
+        prev = std::exchange(node, next);
+    }
+}
+
+// No Sonar cpp:S134 b/c this is the main in-order returning traversal tool - maintainability is not a concern here.
+template 
+template 
+auto Node::traverseInOrderImpl(DerivedT* const root, const Vis& visitor, const bool reverse) -> Result
+{
+    NodeT* node = root;
+    NodeT* prev = nullptr;
+
+    while (nullptr != node)
+    {
+        NodeT* next = node->getParentNode();
+
+        // Did we come down to this node from `prev`?
+        if (prev == next)
+        {
+            if (auto* const left = node->lr[reverse])
+            {
+                next = left;
+            }
+            else
+            {
+                // NOLINTNEXTLINE(*-qualified-auto)
+                if (auto t = visitor(*down(node)))  // NOSONAR cpp:S134
+                {
+                    return t;
+                }
+
+                if (auto* const right = node->lr[!reverse])  // NOSONAR cpp:S134
+                {
+                    next = right;
+                }
+            }
+        }
+        // Did we come up to this node from the left child?
+        else if (prev == node->lr[reverse])
+        {
+            if (auto t = visitor(*down(node)))  // NOLINT(*-qualified-auto)
+            {
+                return t;
+            }
+
+            if (auto* const right = node->lr[!reverse])
+            {
+                next = right;
+            }
+        }
+        else
+        {
+            // next has already been set to the parent node.
+        }
+
+        prev = std::exchange(node, next);
+    }
+    return Result{};
+}
+
+template 
+template 
+void Node::traversePostOrderImpl(DerivedT* const root, const Vis& visitor, const bool reverse)
+{
+    NodeT* node = root;
+    NodeT* prev = nullptr;
+
+    while (nullptr != node)
+    {
+        NodeT* next = node->getParentNode();
+
+        // Did we come down to this node from `prev`?
+        if (prev == next)
+        {
+            if (auto* const left = node->lr[reverse])
+            {
+                next = left;
+            }
+            else if (auto* const right = node->lr[!reverse])
+            {
+                next = right;
+            }
+            else
+            {
+                visitor(*down(node));
+            }
+        }
+        // Did we come up to this node from the left child?
+        else if (prev == node->lr[reverse])
+        {
+            if (auto* const right = node->lr[!reverse])
+            {
+                next = right;
+            }
+            else
+            {
+                visitor(*down(node));
+            }
+        }
+        // We came up to this node from the right child.
+        else
+        {
+            visitor(*down(node));
+        }
+
+        prev = std::exchange(node, next);
+    }
+}
+
 /// This is a very simple convenience wrapper that is entirely optional to use.
 /// It simply keeps a single root pointer of the tree. The methods are mere wrappers over the static methods
 /// defined in the Node<> template class, such that the node pointer kept in the instance of this class is passed
@@ -543,7 +734,6 @@ class Tree final  // NOSONAR cpp:S3624
     using NodeType    = ::cavl::Node;
     using DerivedType = Derived;
 
-    explicit Tree(Derived* const root) : root_(root) {}
     Tree()  = default;
     ~Tree() = default;
 
@@ -552,16 +742,14 @@ class Tree final  // NOSONAR cpp:S3624
     auto operator=(const Tree&) -> Tree& = delete;
 
     /// Trees can be easily moved in constant time. This does not actually affect the tree itself, only this object.
-    Tree(Tree&& other) noexcept : root_(other.root_)
+    Tree(Tree&& other) noexcept : origin_node_{std::move(other.origin_node_)}
     {
         CAVL_ASSERT(!traversal_in_progress_);  // Cannot modify the tree while it is being traversed.
-        other.root_ = nullptr;
     }
     auto operator=(Tree&& other) noexcept -> Tree&
     {
         CAVL_ASSERT(!traversal_in_progress_);  // Cannot modify the tree while it is being traversed.
-        root_       = other.root_;
-        other.root_ = nullptr;
+        origin_node_ = std::move(other.origin_node_);
         return *this;
     }
 
@@ -569,18 +757,18 @@ class Tree final  // NOSONAR cpp:S3624
     template 
     auto search(const Pre& predicate) noexcept -> Derived*
     {
-        return NodeType::template search
(*this, predicate);
+        return NodeType::template search
(getRootNode(), predicate);
     }
     template 
     auto search(const Pre& predicate) const noexcept -> const Derived*
     {
-        return NodeType::template search
(*this, predicate);
+        return NodeType::template search
(getRootNode(), predicate);
     }
     template 
     auto search(const Pre& predicate, const Fac& factory) -> std::tuple
     {
         CAVL_ASSERT(!traversal_in_progress_);  // Cannot modify the tree while it is being traversed.
-        return NodeType::template search(root_, predicate, factory);
+        return NodeType::template search(origin_node_, predicate, factory);
     }
 
     /// Wraps NodeType<>::remove().
@@ -589,7 +777,7 @@ class Tree final  // NOSONAR cpp:S3624
     void remove(NodeType* const node) noexcept  // NOSONAR cpp:S6936
     {
         CAVL_ASSERT(!traversal_in_progress_);  // Cannot modify the tree while it is being traversed.
-        NodeType::remove(root_, node);
+        NodeType::remove(origin_node_, node);
     }
 
     /// Wraps NodeType<>::min/max().
@@ -598,18 +786,32 @@ class Tree final  // NOSONAR cpp:S3624
     auto min() const noexcept -> const Derived* { return NodeType::min(*this); }
     auto max() const noexcept -> const Derived* { return NodeType::max(*this); }
 
-    /// Wraps NodeType<>::traverse().
+    /// Wraps NodeType<>::traverseInOrder().
     template 
-    auto traverse(const Vis& visitor, const bool reverse = false)
+    auto traverseInOrder(const Vis& visitor, const bool reverse = false)
     {
         const TraversalIndicatorUpdater upd(*this);
-        return NodeType::template traverse(*this, visitor, reverse);
+        return NodeType::template traverseInOrder(*this, visitor, reverse);
     }
     template 
-    auto traverse(const Vis& visitor, const bool reverse = false) const
+    auto traverseInOrder(const Vis& visitor, const bool reverse = false) const
     {
         const TraversalIndicatorUpdater upd(*this);
-        return NodeType::template traverse(*this, visitor, reverse);
+        return NodeType::template traverseInOrder(*this, visitor, reverse);
+    }
+
+    /// Wraps NodeType<>::traversePostOrder().
+    template 
+    void traversePostOrder(const Vis& visitor, const bool reverse = false)
+    {
+        const TraversalIndicatorUpdater upd(*this);
+        NodeType::template traversePostOrder(*this, visitor, reverse);
+    }
+    template 
+    void traversePostOrder(const Vis& visitor, const bool reverse = false) const
+    {
+        const TraversalIndicatorUpdater upd(*this);
+        NodeType::template traversePostOrder(*this, visitor, reverse);
     }
 
     /// Normally these are not needed except if advanced introspection is desired.
@@ -618,12 +820,12 @@ class Tree final  // NOSONAR cpp:S3624
     // NOLINTNEXTLINE(google-explicit-constructor,hicpp-explicit-conversions)
     operator Derived*() noexcept  // NOSONAR cpp:S1709
     {
-        return root_;
+        return getRootNode();
     }
     // NOLINTNEXTLINE(google-explicit-constructor,hicpp-explicit-conversions)
     operator const Derived*() const noexcept  // NOSONAR cpp:S1709
     {
-        return root_;
+        return getRootNode();
     }
 
     /// Access i-th element of the tree in linear time. Returns nullptr if the index is out of bounds.
@@ -631,25 +833,25 @@ class Tree final  // NOSONAR cpp:S3624
     {
         std::size_t i = index;
         // No Sonar cpp:S881 b/c this decrement is pretty much straightforward - no maintenance concerns.
-        return traverse([&i](auto& x) { return (i-- == 0) ? &x : nullptr; });  // NOSONAR cpp:S881
+        return traverseInOrder([&i](auto& x) { return (i-- == 0) ? &x : nullptr; });  // NOSONAR cpp:S881
     }
     auto operator[](const std::size_t index) const -> const Derived*
     {
         std::size_t i = index;
         // No Sonar cpp:S881 b/c this decrement is pretty much straightforward - no maintenance concerns.
-        return traverse([&i](const auto& x) { return (i-- == 0) ? &x : nullptr; });  // NOSONAR cpp:S881
+        return traverseInOrder([&i](const auto& x) { return (i-- == 0) ? &x : nullptr; });  // NOSONAR cpp:S881
     }
 
-    /// Beware that this convenience method has linear complexity and uses recursion. Use responsibly.
+    /// Beware that this convenience method has linear complexity. Use responsibly.
     auto size() const noexcept
     {
         auto i = 0UL;
-        traverse([&i](auto& /*unused*/) { i++; });
+        traverseInOrder([&i](auto& /*unused*/) { i++; });
         return i;
     }
 
     /// Unlike size(), this one is constant-complexity.
-    auto empty() const noexcept { return root_ == nullptr; }
+    auto empty() const noexcept { return getRootNode() == nullptr; }
 
 private:
     static_assert(!std::is_polymorphic::value,
@@ -657,7 +859,7 @@ class Tree final  // NOSONAR cpp:S3624
     static_assert(std::is_same, typename NodeType::TreeType>::value, "Internal check: Bad type alias");
 
     /// We use a simple boolean flag instead of a nesting counter to avoid race conditions on the counter update.
-    /// This implies that in the case of concurrent or recursive traversal (more than one call to traverse() within
+    /// This implies that in the case of concurrent or recursive traversal (more than one call to traverseXxx() within
     /// the same call stack) we may occasionally fail to detect a bona fide case of a race condition, but this is
     /// acceptable because the purpose of this feature is to provide a mere best-effort data race detection.
     ///
@@ -677,7 +879,17 @@ class Tree final  // NOSONAR cpp:S3624
         const Tree& that;
     };
 
-    Derived* root_ = nullptr;
+    // root node pointer is stored in the origin_node_ left child.
+    auto getRootNode() noexcept -> Derived* { return origin_node_.getChildNode(false); }
+    auto getRootNode() const noexcept -> const Derived* { return origin_node_.getChildNode(false); }
+
+    // This a "fake" node, is not part of the tree itself, but it is used to store the root node pointer.
+    // The root node pointer is stored in the left child (see `getRootNode` methods).
+    // This is the only node which has the `up` pointer set to `nullptr`;
+    // all other "real" nodes always have non-null `up` pointer,
+    // including the root node whos `up` points to this origin node (see `isRoot` method).
+    Node origin_node_{};
+
     // No Sonar cpp:S4963 b/c of implicit modification by the `TraversalIndicatorUpdater` RAII class,
     // even for `const` instance of the `Tree` class (hence the `mutable volatile` keywords).
     mutable volatile bool traversal_in_progress_ = false;  // NOSONAR cpp:S3687
diff --git a/c++/test.cpp b/c++/test.cpp
index de9b8a6..9d30ae2 100644
--- a/c++/test.cpp
+++ b/c++/test.cpp
@@ -49,12 +49,15 @@ class My : public cavl::Node
 public:
     explicit My(const std::uint16_t v) : value(v) {}
     using Self = cavl::Node;
+    using Self::isLinked;
+    using Self::isRoot;
     using Self::getChildNode;
     using Self::getParentNode;
     using Self::getBalanceFactor;
     using Self::search;
     using Self::remove;
-    using Self::traverse;
+    using Self::traverseInOrder;
+    using Self::traversePostOrder;
     using Self::min;
     using Self::max;
 
@@ -103,12 +106,12 @@ NODISCARD auto getHeight(const N* const n) -> std::int8_t  // NOLINT(misc-no-
 
 /// Returns the size if the tree is ordered correctly, otherwise SIZE_MAX.
 template 
-NODISCARD std::size_t checkOrdering(const N* const root)
+NODISCARD std::size_t checkNormalOrdering(const N* const root)
 {
     const N* prev  = nullptr;
     bool        valid = true;
     std::size_t size  = 0;
-    T::traverse(root, [&](const N& nd) {
+    T::traverseInOrder(root, [&](const N& nd) {
         if (prev != nullptr)
         {
             valid = valid && (prev->getValue() < nd.getValue());
@@ -116,8 +119,52 @@ NODISCARD std::size_t checkOrdering(const N* const root)
         prev = &nd;
         size++;
     });
+
     return valid ? size : std::numeric_limits::max();
 }
+template 
+std::size_t checkReverseOrdering(const N* const root)
+{
+    const N* prev  = nullptr;
+    bool        valid = true;
+    std::size_t size  = 0;
+    T::traverseInOrder(
+        root,
+        [&](const N& nd) {
+            if (prev != nullptr)
+            {
+                valid = valid && (prev->getValue() > nd.getValue());
+            }
+            prev = &nd;
+            size++;
+
+            // Fake `return` to cover other `traverseInOrder` overload (the returning one).
+            return false;
+        },
+        true /* reverse */);
+
+    return valid ? size : std::numeric_limits::max();
+}
+template 
+NODISCARD std::size_t checkOrdering(const N* const root)
+{
+    const std::size_t ordered = checkNormalOrdering(root);
+    const std::size_t reverse = checkReverseOrdering(root);
+    return (ordered == reverse) ? ordered : std::numeric_limits::max();
+}
+
+template 
+void checkPostOrdering(const N* const root, const std::vector& expected, const bool reverse = false)
+{
+    std::vector order;
+    T::traversePostOrder(
+        root, [&](const N& nd) { order.push_back(nd.getValue()); }, reverse);
+    TEST_ASSERT_EQUAL(expected.size(), order.size());
+    if (!order.empty())
+    {
+        TEST_ASSERT_EQUAL_UINT16_ARRAY(expected.data(), order.data(), order.size());
+    }
+}
 
 template 
 // NOLINTNEXTLINE(misc-no-recursion)
@@ -173,13 +220,13 @@ NODISCARD auto toGraphviz(const cavl::Tree& tr) -> std::string
        << "node[style=filled,shape=circle,fontcolor=white,penwidth=0,fontname=\"monospace\",fixedsize=1,fontsize=18];\n"
        << "edge[arrowhead=none,penwidth=2];\n"
        << "nodesep=0.0;ranksep=0.3;splines=false;\n";
-    tr.traverse([&](const typename cavl::Tree::DerivedType& x) {
+    tr.traverseInOrder([&](const typename cavl::Tree::DerivedType& x) {
         const char* const fill_color =  // NOLINTNEXTLINE(*-avoid-nested-conditional-operator)
             (x.getBalanceFactor() == 0) ? "black" : ((x.getBalanceFactor() > 0) ? "orange" : "blue");
         ss << x.getValue() << "[fillcolor=" << fill_color << "];";
     });
     ss << "\n";
-    tr.traverse([&](const typename cavl::Tree::DerivedType& x) {
+    tr.traverseInOrder([&](const typename cavl::Tree::DerivedType& x) {
         if (const auto* const ch = x.getChildNode(false))
         {
             ss << x.getValue() << ":sw->" << ch->getValue() << ":n;";
@@ -199,7 +246,7 @@ auto getRandomByte()
 }
 
 template 
-void testManual(const std::function& factory)
+void testManual(const std::function& factory, const std::function& node_mover)
 {
     using TreeType = typename N::TreeType;
     std::vector t;
@@ -225,7 +272,9 @@ void testManual(const std::function& factory)
         const auto pred = [&](const N& v) { return t.at(i)->getValue() - v.getValue(); };
         TEST_ASSERT_NULL(tr.search(pred));
         TEST_ASSERT_NULL(static_cast(tr).search(pred));
+        TEST_ASSERT_FALSE(t[i]->isLinked());
         auto result = tr.search(pred, [&]() { return t[i]; });
+        TEST_ASSERT_TRUE(t[i]->isLinked());
         TEST_ASSERT_EQUAL(t[i], std::get<0>(result));
         TEST_ASSERT_FALSE(std::get<1>(result));
         TEST_ASSERT_EQUAL(t[i], tr.search(pred));
@@ -255,7 +304,7 @@ void testManual(const std::function& factory)
     // Check composition -- ensure that every element is in the tree and it is there exactly once.
     {
         bool seen[32]{};
-        tr.traverse([&](const N& n) {
+        tr.traverseInOrder([&](const N& n) {
             TEST_ASSERT_FALSE(seen[n.getValue()]);
             seen[n.getValue()] = true;
         });
@@ -276,6 +325,24 @@ void testManual(const std::function& factory)
         TEST_ASSERT_EQUAL_INT64(i, tr[i - 1]->getValue());
         TEST_ASSERT_EQUAL_INT64(i, static_cast(tr)[i - 1]->getValue());
     }
+    checkPostOrdering(tr, {1,  3,  2,  5,  7,  6,  4,  9,  11, 10, 13, 15, 14, 12, 8, 17,
+                              19, 18, 21, 23, 22, 20, 25, 27, 26, 29, 31, 30, 28, 24, 16});
+    checkPostOrdering(tr,
+                         {31, 29, 30, 27, 25, 26, 28, 23, 21, 22, 19, 17, 18, 20, 24, 15,
+                          13, 14, 11, 9,  10, 12, 7,  5,  6,  3,  1,  2,  4,  8,  16},
+                         true);
+    TEST_ASSERT_TRUE(t[16]->isRoot());
+    TEST_ASSERT_FALSE(t[24]->isRoot());
+
+    // MOVE 16, 18 & 23
+    t[16] = node_mover(t[16]);
+    t[18] = node_mover(t[18]);
+    t[23] = node_mover(t[23]);
+    TEST_ASSERT_TRUE(t[16]->isRoot());
+    TEST_ASSERT_FALSE(t[18]->isRoot());
+    TEST_ASSERT_TRUE(t[18]->isLinked());
+    TEST_ASSERT_FALSE(t[23]->isRoot());
+    TEST_ASSERT_TRUE(t[23]->isLinked());
 
     // REMOVE 24
     //                               16
@@ -300,6 +367,11 @@ void testManual(const std::function& factory)
     TEST_ASSERT_NULL(findBrokenBalanceFactor(tr));
     TEST_ASSERT_NULL(findBrokenAncestry(tr));
     TEST_ASSERT_EQUAL(30, checkOrdering(tr));
+    TEST_ASSERT_TRUE(t[16]->isRoot());
+    TEST_ASSERT_FALSE(t[24]->isRoot());
+    TEST_ASSERT_FALSE(t[24]->isLinked());
+    checkPostOrdering(tr, {1,  3,  2,  5,  7,  6,  4,  9,  11, 10, 13, 15, 14, 12, 8,
+                              17, 19, 18, 21, 23, 22, 20, 27, 26, 29, 31, 30, 28, 25, 16});
 
     // REMOVE 25
     //                               16
@@ -320,6 +392,11 @@ void testManual(const std::function& factory)
     TEST_ASSERT_NULL(findBrokenBalanceFactor(tr));
     TEST_ASSERT_NULL(findBrokenAncestry(tr));
     TEST_ASSERT_EQUAL(29, checkOrdering(tr));
+    TEST_ASSERT_TRUE(t[16]->isRoot());
+    TEST_ASSERT_FALSE(t[25]->isRoot());
+    TEST_ASSERT_FALSE(t[25]->isLinked());
+    checkPostOrdering(tr, {1,  3,  2,  5,  7,  6,  4,  9,  11, 10, 13, 15, 14, 12, 8,
+                              17, 19, 18, 21, 23, 22, 20, 27, 29, 31, 30, 28, 26, 16});
 
     // REMOVE 26
     //                               16
@@ -341,6 +418,11 @@ void testManual(const std::function& factory)
     TEST_ASSERT_NULL(findBrokenBalanceFactor(tr));
     TEST_ASSERT_NULL(findBrokenAncestry(tr));
     TEST_ASSERT_EQUAL(28, checkOrdering(tr));
+    TEST_ASSERT_TRUE(t[16]->isRoot());
+    TEST_ASSERT_FALSE(t[26]->isRoot());
+    TEST_ASSERT_FALSE(t[26]->isLinked());
+    checkPostOrdering(tr, {1, 3,  2,  5,  7,  6,  4,  9,  11, 10, 13, 15, 14, 12,
+                              8, 17, 19, 18, 21, 23, 22, 20, 29, 28, 31, 30, 27, 16});
 
     // REMOVE 20
     //                               16
@@ -361,6 +443,11 @@ void testManual(const std::function& factory)
     TEST_ASSERT_NULL(findBrokenBalanceFactor(tr));
     TEST_ASSERT_NULL(findBrokenAncestry(tr));
     TEST_ASSERT_EQUAL(27, checkOrdering(tr));
+    TEST_ASSERT_TRUE(t[16]->isRoot());
+    TEST_ASSERT_FALSE(t[20]->isRoot());
+    TEST_ASSERT_FALSE(t[20]->isLinked());
+    checkPostOrdering(tr, {1, 3,  2,  5,  7,  6,  4,  9,  11, 10, 13, 15, 14, 12,
+                              8, 17, 19, 18, 23, 22, 21, 29, 28, 31, 30, 27, 16});
 
     // REMOVE 27
     //                               16
@@ -381,6 +468,11 @@ void testManual(const std::function& factory)
     TEST_ASSERT_NULL(findBrokenBalanceFactor(tr));
     TEST_ASSERT_NULL(findBrokenAncestry(tr));
     TEST_ASSERT_EQUAL(26, checkOrdering(tr));
+    TEST_ASSERT_TRUE(t[16]->isRoot());
+    TEST_ASSERT_FALSE(t[27]->isRoot());
+    TEST_ASSERT_FALSE(t[27]->isLinked());
+    checkPostOrdering(tr, {1,  3, 2,  5,  7,  6,  4,  9,  11, 10, 13, 15, 14,
+                              12, 8, 17, 19, 18, 23, 22, 21, 29, 31, 30, 28, 16});
 
     // REMOVE 28
     //                               16
@@ -401,6 +493,11 @@ void testManual(const std::function& factory)
     TEST_ASSERT_NULL(findBrokenBalanceFactor(tr));
     TEST_ASSERT_NULL(findBrokenAncestry(tr));
     TEST_ASSERT_EQUAL(25, checkOrdering(tr));
+    TEST_ASSERT_TRUE(t[16]->isRoot());
+    TEST_ASSERT_FALSE(t[28]->isRoot());
+    TEST_ASSERT_FALSE(t[28]->isLinked());
+    checkPostOrdering(tr,
+                         {1, 3, 2, 5, 7, 6, 4, 9, 11, 10, 13, 15, 14, 12, 8, 17, 19, 18, 23, 22, 21, 31, 30, 29, 16});
 
     // REMOVE 29; UNBALANCED TREE BEFORE ROTATION:
     //                               16
@@ -435,6 +532,10 @@ void testManual(const std::function& factory)
     TEST_ASSERT_NULL(findBrokenBalanceFactor(tr));
     TEST_ASSERT_NULL(findBrokenAncestry(tr));
     TEST_ASSERT_EQUAL(24, checkOrdering(tr));
+    TEST_ASSERT_TRUE(t[16]->isRoot());
+    TEST_ASSERT_FALSE(t[29]->isRoot());
+    TEST_ASSERT_FALSE(t[29]->isLinked());
+    checkPostOrdering(tr, {1, 3, 2, 5, 7, 6, 4, 9, 11, 10, 13, 15, 14, 12, 8, 17, 19, 18, 23, 22, 31, 30, 21, 16});
 
     // REMOVE 8
     //                               16
@@ -455,6 +556,10 @@ void testManual(const std::function& factory)
     TEST_ASSERT_NULL(findBrokenBalanceFactor(tr));
     TEST_ASSERT_NULL(findBrokenAncestry(tr));
     TEST_ASSERT_EQUAL(23, checkOrdering(tr));
+    TEST_ASSERT_TRUE(t[16]->isRoot());
+    TEST_ASSERT_FALSE(t[8]->isRoot());
+    TEST_ASSERT_FALSE(t[8]->isLinked());
+    checkPostOrdering(tr, {1, 3, 2, 5, 7, 6, 4, 11, 10, 13, 15, 14, 12, 9, 17, 19, 18, 23, 22, 31, 30, 21, 16});
 
     // REMOVE 9
     //                               16
@@ -475,6 +580,10 @@ void testManual(const std::function& factory)
     TEST_ASSERT_NULL(findBrokenBalanceFactor(tr));
     TEST_ASSERT_NULL(findBrokenAncestry(tr));
     TEST_ASSERT_EQUAL(22, checkOrdering(tr));
+    TEST_ASSERT_TRUE(t[16]->isRoot());
+    TEST_ASSERT_FALSE(t[9]->isRoot());
+    TEST_ASSERT_FALSE(t[9]->isLinked());
+    checkPostOrdering(tr, {1, 3, 2, 5, 7, 6, 4, 11, 13, 15, 14, 12, 10, 17, 19, 18, 23, 22, 31, 30, 21, 16});
 
     // REMOVE 1
     //                               16
@@ -494,6 +603,10 @@ void testManual(const std::function& factory)
     TEST_ASSERT_NULL(findBrokenBalanceFactor(tr));
     TEST_ASSERT_NULL(findBrokenAncestry(tr));
     TEST_ASSERT_EQUAL(21, checkOrdering(tr));
+    TEST_ASSERT_TRUE(t[16]->isRoot());
+    TEST_ASSERT_FALSE(t[1]->isRoot());
+    TEST_ASSERT_FALSE(t[1]->isLinked());
+    checkPostOrdering(tr, {3, 2, 5, 7, 6, 4, 11, 13, 15, 14, 12, 10, 17, 19, 18, 23, 22, 31, 30, 21, 16});
 
     // REMOVE 16, the tree got new root.
     //                               17
@@ -517,6 +630,10 @@ void testManual(const std::function& factory)
     TEST_ASSERT_NULL(findBrokenBalanceFactor(tr));
     TEST_ASSERT_NULL(findBrokenAncestry(tr));
     TEST_ASSERT_EQUAL(20, checkOrdering(tr));
+    TEST_ASSERT_TRUE(t[17]->isRoot());
+    TEST_ASSERT_FALSE(t[16]->isRoot());
+    TEST_ASSERT_FALSE(t[16]->isLinked());
+    checkPostOrdering(tr, {3, 2, 5, 7, 6, 4, 11, 13, 15, 14, 12, 10, 19, 18, 23, 22, 31, 30, 21, 17});
 
     // REMOVE 22, only has one child.
     //                               17
@@ -537,6 +654,10 @@ void testManual(const std::function& factory)
     TEST_ASSERT_NULL(findBrokenBalanceFactor(tr));
     TEST_ASSERT_NULL(findBrokenAncestry(tr));
     TEST_ASSERT_EQUAL(19, checkOrdering(tr));
+    TEST_ASSERT_TRUE(t[17]->isRoot());
+    TEST_ASSERT_FALSE(t[22]->isRoot());
+    TEST_ASSERT_FALSE(t[22]->isLinked());
+    checkPostOrdering(tr, {3, 2, 5, 7, 6, 4, 11, 13, 15, 14, 12, 10, 19, 18, 23, 31, 30, 21, 17});
 
     // Print intermediate state for inspection. Be sure to compare it against the above diagram for extra paranoia.
     std::cout << toGraphviz(tr) << std::endl;
@@ -590,6 +711,9 @@ void testManual(const std::function& factory)
     TEST_ASSERT_EQUAL(t.at(30), static_cast(tr).max());
     TEST_ASSERT_EQUAL(t[17], static_cast(tr));
     TEST_ASSERT_EQUAL(7, tr.size());
+    TEST_ASSERT_TRUE(t[17]->isRoot());
+    checkPostOrdering(tr, {4, 12, 10, 18, 30, 21, 17});
+    checkPostOrdering(tr, {30, 18, 21, 12, 4, 10, 17}, true);
 
     // REMOVE 10, 21.
     //                               17
@@ -615,6 +739,13 @@ void testManual(const std::function& factory)
     TEST_ASSERT_EQUAL(t.at(30), static_cast(tr).max());
     TEST_ASSERT_EQUAL(t[17], static_cast(tr));
     TEST_ASSERT_EQUAL(5, tr.size());
+    TEST_ASSERT_TRUE(t[17]->isRoot());
+    TEST_ASSERT_FALSE(t[10]->isRoot());
+    TEST_ASSERT_FALSE(t[10]->isLinked());
+    TEST_ASSERT_FALSE(t[21]->isRoot());
+    TEST_ASSERT_FALSE(t[21]->isLinked());
+    checkPostOrdering(tr, {4, 12, 18, 30, 17});
+    checkPostOrdering(tr, {18, 30, 4, 12, 17}, true);
 
     // REMOVE 12, 18.
     //                               17
@@ -636,6 +767,13 @@ void testManual(const std::function& factory)
     TEST_ASSERT_EQUAL(t.at(30), static_cast(tr).max());
     TEST_ASSERT_EQUAL(t[17], static_cast(tr));
     TEST_ASSERT_EQUAL(3, tr.size());
+    TEST_ASSERT_TRUE(t[17]->isRoot());
+    TEST_ASSERT_FALSE(t[12]->isRoot());
+    TEST_ASSERT_FALSE(t[12]->isLinked());
+    TEST_ASSERT_FALSE(t[18]->isRoot());
+    TEST_ASSERT_FALSE(t[18]->isLinked());
+    checkPostOrdering(tr, {4, 30, 17});
+    checkPostOrdering(tr, {30, 4, 17}, true);
 
     // REMOVE 17. 30 is the new root.
     //                               30
@@ -655,6 +793,11 @@ void testManual(const std::function& factory)
     TEST_ASSERT_EQUAL(t.at(30), static_cast(tr).max());
     TEST_ASSERT_EQUAL(t[30], static_cast(tr));
     TEST_ASSERT_EQUAL(2, tr.size());
+    TEST_ASSERT_TRUE(t[30]->isRoot());
+    TEST_ASSERT_FALSE(t[17]->isRoot());
+    TEST_ASSERT_FALSE(t[17]->isLinked());
+    checkPostOrdering(tr, {4, 30});
+    checkPostOrdering(tr, {4, 30}, true);
 
     // REMOVE 30. 4 is the only node left.
     //                               4
@@ -671,6 +814,11 @@ void testManual(const std::function& factory)
     TEST_ASSERT_EQUAL(t.at(4), static_cast(tr).max());
     TEST_ASSERT_EQUAL(t[4], static_cast(tr));
     TEST_ASSERT_EQUAL(1, tr.size());
+    TEST_ASSERT_TRUE(t[4]->isRoot());
+    TEST_ASSERT_FALSE(t[30]->isRoot());
+    TEST_ASSERT_FALSE(t[30]->isLinked());
+    checkPostOrdering(tr, {4});
+    checkPostOrdering(tr, {4}, true);
 
     // Check the move assignment and move constructor of the tree.
     TreeType tr2(std::move(tr));
@@ -682,6 +830,7 @@ void testManual(const std::function& factory)
     TEST_ASSERT_EQUAL(t.at(4), static_cast(tr3));  // Moved.
     TEST_ASSERT_NULL(static_cast(tr2));            // NOLINT use after move is intentional.
     TEST_ASSERT_EQUAL(1, tr3.size());
+    TEST_ASSERT_TRUE(t[4]->isRoot());
 
     // Try various methods on empty tree (including `const` one).
     //
@@ -694,7 +843,11 @@ void testManual(const std::function& factory)
     TEST_ASSERT_EQUAL(0, tr4_const.size());
     TEST_ASSERT_EQUAL(nullptr, tr4_const.min());
     TEST_ASSERT_EQUAL(nullptr, tr4_const.max());
-    TEST_ASSERT_EQUAL(0, tr4_const.traverse([](const N&) { return 13; }));
+    TEST_ASSERT_EQUAL(0, tr4_const.traverseInOrder([](const N&) { return 13; }));
+    TEST_ASSERT_FALSE(t[4]->isRoot());
+    TEST_ASSERT_FALSE(t[4]->isLinked());
+    checkPostOrdering(tr4_const, {});
+    checkPostOrdering(tr4_const, {}, true);
 
     // Clean up manually to reduce boilerplate in the tests. This is super sloppy but OK for a basic test suite.
     for (auto* const x : t)
@@ -725,7 +878,7 @@ void testRandomized()
         TEST_ASSERT_NULL(findBrokenAncestry(root));
         TEST_ASSERT_EQUAL(size, checkOrdering(root));
         std::array new_mask{};
-        root.traverse([&](const My& node) { new_mask.at(node.getValue()) = true; });
+        root.traverseInOrder([&](const My& node) { new_mask.at(node.getValue()) = true; });
         TEST_ASSERT_EQUAL(mask, new_mask);  // Otherwise, the contents of the tree does not match our expectations.
     };
     validate();
@@ -804,9 +957,17 @@ void testRandomized()
 
 void testManualMy()
 {
-    testManual([](const std::uint16_t x) {
-        return new My(x);  // NOLINT
-    });
+    testManual(
+        [](const std::uint16_t x) {
+            return new My(x);  // NOLINT
+        },
+        [](My* const old_node) {
+            const auto value    = old_node->getValue();
+            My* const  new_node = new My(std::move(*old_node));  // NOLINT(*-owning-memory)
+            TEST_ASSERT_EQUAL(value, new_node->getValue());
+            delete old_node;  // NOLINT(*-owning-memory)
+            return new_node;
+        });
 }
 
 /// Ensure that polymorphic types can be used with the tree. The tree node type itself is not polymorphic!
@@ -814,27 +975,33 @@ class V : public cavl::Node
 {
 public:
     using Self = cavl::Node;
+    using Self::isLinked;
+    using Self::isRoot;
     using Self::getChildNode;
     using Self::getParentNode;
     using Self::getBalanceFactor;
     using Self::search;
     using Self::remove;
-    using Self::traverse;
+    using Self::traverseInOrder;
+    using Self::traversePostOrder;
     using Self::min;
     using Self::max;
 
     V()                    = default;
     virtual ~V()           = default;
     V(const V&)            = delete;
-    V(V&&)                 = delete;
     V& operator=(const V&) = delete;
-    V& operator=(V&&)      = delete;
 
+    V& operator=(V&&) noexcept = default;
+    V(V&&) noexcept            = default;
+
+    NODISCARD virtual V*   clone()                           = 0;
     NODISCARD virtual auto getValue() const -> std::uint16_t = 0;
 
 private:
     using E = struct
     {};
+    UNUSED E root_ptr;
     UNUSED E up;
     UNUSED E lr;
     UNUSED E bf;
@@ -848,6 +1015,10 @@ template 
 class VValue : public VValue(Value - 1)>
 {
 public:
+    NODISCARD V* clone() override
+    {
+        return new VValue(std::move(*this));  // NOLINT(*-owning-memory)
+    }
     NODISCARD auto getValue() const -> std::uint16_t override
     {
         return static_cast(VValue(Value - 1)>::getValue() + 1);
@@ -857,6 +1028,10 @@ template <>
 class VValue<0> : public V
 {
 public:
+    NODISCARD V* clone() override
+    {
+        return new VValue(std::move(*this));  // NOLINT(*-owning-memory)
+    }
     NODISCARD auto getValue() const -> std::uint16_t override { return 0; }
 };
 
@@ -888,7 +1063,11 @@ auto makeV(const std::uint8_t val) -> V*
 
 void testManualV()
 {
-    testManual(&makeV<>);
+    testManual(&makeV<>, [](V* const old_node) {  //
+        auto* const new_node = old_node->clone();
+        delete old_node;  // NOLINT(*-owning-memory)
+        return new_node;
+    });
 }
 
 }  // namespace