diff --git a/c++/cavl.hpp b/c++/cavl.hpp index bc4893b..06b2ac6 100644 --- a/c++/cavl.hpp +++ b/c++/cavl.hpp @@ -95,12 +95,13 @@ class Node // NOSONAR cpp:S1448 cpp:S4963 ~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; } - auto getRootNodePtr() noexcept -> Derived** { return root_ptr; } /// Find a node for which the predicate returns zero, or nullptr if there is no such node or the tree is empty. /// The predicate is invoked with a single argument which is a constant reference to Derived. @@ -109,55 +110,39 @@ class Node // NOSONAR cpp:S1448 cpp:S4963 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();
@@ -229,24 +214,18 @@ class Node  // NOSONAR cpp:S1448 cpp:S4963
 private:
     void moveFrom(Node& other) noexcept
     {
-        root_ptr = other.root_ptr;
-        up       = other.up;
-        lr[0]    = other.lr[0];
-        lr[1]    = other.lr[1];
-        bf       = other.bf;
+        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)
         {
             up->lr[up->lr[1] == &other] = this;
         }
-        else
-        {
-            if (nullptr != root_ptr)
-            {
-                *root_ptr = down(this);
-            }
-        }
-
         if (nullptr != lr[0])
         {
             lr[0]->up = this;
@@ -255,21 +234,17 @@ class Node  // NOSONAR cpp:S1448 cpp:S4963
         {
             lr[1]->up = this;
         }
-
-        other.unlink();
     }
 
     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;
@@ -289,13 +264,31 @@ class Node  // NOSONAR cpp:S1448 cpp:S4963
     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
     {
-        root_ptr = nullptr;
-        up       = nullptr;
-        lr[0]    = nullptr;
-        lr[1]    = nullptr;
-        bf       = 0;
+        up    = nullptr;
+        lr[0] = nullptr;
+        lr[1] = nullptr;
+        bf    = 0;
     }
 
     static auto extremum(Node* const root, const bool maximum) noexcept -> Derived*
@@ -327,23 +320,27 @@ class Node  // NOSONAR cpp:S1448 cpp:S4963
 
     friend class Tree;
 
-    Derived**            root_ptr = nullptr;
-    Node*                up       = nullptr;
+    Node*                up = nullptr;
     std::array lr{};
     std::int8_t          bf = 0;
 };
 
 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;
@@ -360,26 +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;
-    out->root_ptr = &root;
     if (Node* const rt = out->retraceOnGrowth())
     {
-        root = down(rt);
+        root = rt;
     }
     return std::make_tuple(down(out), false);
 }
@@ -387,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.
@@ -406,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
@@ -423,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.
@@ -440,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];
@@ -451,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`.
@@ -475,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;
             }
         }
     }
@@ -484,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));
@@ -545,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.
@@ -572,12 +575,11 @@ void Node::traverseInOrderImpl(DerivedT* const root, const Vis& visitor
 
     while (nullptr != node)
     {
-        NodeT* next = node->up;
+        NodeT* next = node->getParentNode();
 
-        if (prev == node->up)
+        // Did we come down to this node from `prev`?
+        if (prev == next)
         {
-            // We came down to this node from `prev`.
-
             if (auto* const left = node->lr[reverse])
             {
                 next = left;
@@ -592,10 +594,9 @@ void Node::traverseInOrderImpl(DerivedT* const root, const Vis& visitor
                 }
             }
         }
+        // Did we come up to this node from the left child?
         else if (prev == node->lr[reverse])
         {
-            // We came up to this node from the left child.
-
             visitor(*down(node));
 
             if (auto* const right = node->lr[!reverse])
@@ -622,12 +623,11 @@ auto Node::traverseInOrderImpl(DerivedT* const root, const Vis& visitor
 
     while (nullptr != node)
     {
-        NodeT* next = node->up;
+        NodeT* next = node->getParentNode();
 
-        if (prev == node->up)
+        // Did we come down to this node from `prev`?
+        if (prev == next)
         {
-            // We came down to this node from `prev`.
-
             if (auto* const left = node->lr[reverse])
             {
                 next = left;
@@ -646,10 +646,9 @@ auto Node::traverseInOrderImpl(DerivedT* const root, const Vis& visitor
                 }
             }
         }
+        // Did we come up to this node from the left child?
         else if (prev == node->lr[reverse])
         {
-            // We came up to this node from the left child.
-
             if (auto t = visitor(*down(node)))  // NOLINT(*-qualified-auto)
             {
                 return t;
@@ -679,12 +678,11 @@ void Node::traversePostOrderImpl(DerivedT* const root, const Vis& visit
 
     while (nullptr != node)
     {
-        NodeT* next = node->up;
+        NodeT* next = node->getParentNode();
 
-        if (prev == node->up)
+        // Did we come down to this node from `prev`?
+        if (prev == next)
         {
-            // We came down to this node from `prev`.
-
             if (auto* const left = node->lr[reverse])
             {
                 next = left;
@@ -698,10 +696,9 @@ void Node::traversePostOrderImpl(DerivedT* const root, const Vis& visit
                 visitor(*down(node));
             }
         }
+        // Did we come up to this node from the left child?
         else if (prev == node->lr[reverse])
         {
-            // We came up to this node from the left child.
-
             if (auto* const right = node->lr[!reverse])
             {
                 next = right;
@@ -711,10 +708,9 @@ void Node::traversePostOrderImpl(DerivedT* const root, const Vis& visit
                 visitor(*down(node));
             }
         }
+        // We came up to this node from the right child.
         else
         {
-            // We came up to this node from the right child.
-
             visitor(*down(node));
         }
 
@@ -738,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;
 
@@ -747,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;
     }
 
@@ -764,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().
@@ -784,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().
@@ -827,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.
@@ -858,7 +851,7 @@ class Tree final  // NOSONAR cpp:S3624
     }
 
     /// 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,
@@ -886,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 73b3a72..9d30ae2 100644
--- a/c++/test.cpp
+++ b/c++/test.cpp
@@ -49,10 +49,11 @@ 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::getRootNodePtr;
     using Self::search;
     using Self::remove;
     using Self::traverseInOrder;
@@ -271,7 +272,9 @@ void testManual(const std::function& factory, const std::funct
         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));
@@ -328,11 +331,18 @@ void testManual(const std::function& factory, const std::funct
                          {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
@@ -357,6 +367,9 @@ void testManual(const std::function& factory, const std::funct
     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});
 
@@ -379,6 +392,9 @@ void testManual(const std::function& factory, const std::funct
     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});
 
@@ -402,6 +418,9 @@ void testManual(const std::function& factory, const std::funct
     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});
 
@@ -424,6 +443,9 @@ void testManual(const std::function& factory, const std::funct
     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});
 
@@ -446,6 +468,9 @@ void testManual(const std::function& factory, const std::funct
     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});
 
@@ -468,6 +493,9 @@ void testManual(const std::function& factory, const std::funct
     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});
 
@@ -504,6 +532,9 @@ void testManual(const std::function& factory, const std::funct
     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
@@ -525,6 +556,9 @@ void testManual(const std::function& factory, const std::funct
     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
@@ -546,6 +580,9 @@ void testManual(const std::function& factory, const std::funct
     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
@@ -566,6 +603,9 @@ void testManual(const std::function& factory, const std::funct
     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.
@@ -590,6 +630,9 @@ void testManual(const std::function& factory, const std::funct
     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.
@@ -611,6 +654,9 @@ void testManual(const std::function& factory, const std::funct
     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.
@@ -665,6 +711,7 @@ void testManual(const std::function& factory, const std::funct
     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);
 
@@ -692,6 +739,11 @@ void testManual(const std::function& factory, const std::funct
     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);
 
@@ -715,6 +767,11 @@ void testManual(const std::function& factory, const std::funct
     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);
 
@@ -736,6 +793,9 @@ void testManual(const std::function& factory, const std::funct
     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);
 
@@ -754,6 +814,9 @@ void testManual(const std::function& factory, const std::funct
     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);
 
@@ -767,6 +830,7 @@ void testManual(const std::function& factory, const std::funct
     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).
     //
@@ -780,6 +844,8 @@ void testManual(const std::function& factory, const std::funct
     TEST_ASSERT_EQUAL(nullptr, tr4_const.min());
     TEST_ASSERT_EQUAL(nullptr, tr4_const.max());
     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);
 
@@ -897,11 +963,8 @@ void testManualMy()
         },
         [](My* const old_node) {
             const auto value    = old_node->getValue();
-            My** const root_ptr = old_node->getRootNodePtr();
             My* const  new_node = new My(std::move(*old_node));  // NOLINT(*-owning-memory)
             TEST_ASSERT_EQUAL(value, new_node->getValue());
-            TEST_ASSERT_EQUAL(root_ptr, new_node->getRootNodePtr());
-            TEST_ASSERT_EQUAL(nullptr, old_node->getRootNodePtr());
             delete old_node;  // NOLINT(*-owning-memory)
             return new_node;
         });
@@ -912,6 +975,8 @@ 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;