c++binary-search-treecopy-constructormove-constructorrule-of-five

Rule-of-five for a BST class in C++


I am implementing a binary search tree class and was wondering if my move/copy constructors and assignment operators are implemented correctly. (It seems to work correctly, but this is my first time implementing these constructors and assignment operators, I am afraid I might have missed something.)

Here is the code (also in an online compiler): EDIT: Here is updated code based on @Alex Larionov comment:

#include <memory>
#include <iostream>

class BinarySearchTree {

public:
    BinarySearchTree();
    BinarySearchTree(int value);
    BinarySearchTree(const BinarySearchTree& other_tree);
    BinarySearchTree(BinarySearchTree&& other_tree);
    BinarySearchTree& operator=(const BinarySearchTree& other_tree);
    BinarySearchTree& operator=(BinarySearchTree&& other_tree);
    ~BinarySearchTree() = default;

    void clear();

    inline int size() const {
        return tree_size;
    }
    inline bool empty() const {
        return tree_size == 0;
    }

private:
    struct Node {
        int val;
        std::unique_ptr<Node> left = nullptr;
        std::unique_ptr<Node> right = nullptr;

        Node(const int value) :
        val{value},
        left{nullptr},
        right{nullptr}
        {}
    };

    std::unique_ptr<Node> root;
    int tree_size;

    void deep_copy_tree(std::unique_ptr<Node>& dest_node, const std::unique_ptr<Node>& source_node);

};


BinarySearchTree::BinarySearchTree() : root{nullptr}, tree_size{0} {
    std::cout << "BinarySearchTree() constructor\n";
}

BinarySearchTree::BinarySearchTree(int value) : root{std::make_unique<Node>(value)}, tree_size{1} {
    std::cout << "BinarySearchTree(int value) constructor\n";
}

BinarySearchTree::BinarySearchTree(const BinarySearchTree& other_tree) : root{nullptr}, tree_size{0} {
    std::cout << "Copy constructor\n";
    if (other_tree.tree_size == 0) return;
    tree_size = other_tree.tree_size;
    deep_copy_tree(root, other_tree.root);
}

BinarySearchTree::BinarySearchTree(BinarySearchTree&& other_tree) :
root(std::exchange(other_tree.root, nullptr)), tree_size(std::exchange(other_tree.tree_size, 0)) {
        std::cout << "Move constructor\n";
}

BinarySearchTree& BinarySearchTree::operator=(const BinarySearchTree& other_tree) {
    std::cout << "Copy assignment operator\n";
    clear();
    tree_size = other_tree.tree_size;
    deep_copy_tree(root, other_tree.root);
    return *this;
}

// EDIT: updated based on @Alex Larionov comment
BinarySearchTree& BinarySearchTree::operator=(BinarySearchTree&& other_tree) {
    std::cout << "Move assignment operator\n";
    clear();
    tree_size = other_tree.tree_size;
    other_tree.tree_size = 0;
    root = std::move(other_tree.root);

    return *this;
}
/*BinarySearchTree& BinarySearchTree::operator=(BinarySearchTree&& other_tree) {
    std::cout << "Move assignment operator\n";
    clear();
    tree_size = other_tree.tree_size;
    deep_copy_tree(root, other_tree.root);

    other_tree.tree_size = 0;
    other_tree.root = nullptr;

    return *this;
}*/


void BinarySearchTree::clear() {
    root = nullptr;
    tree_size = 0;
}

void BinarySearchTree::deep_copy_tree(std::unique_ptr<Node>& dest_node, const std::unique_ptr<Node>& source_node) {
    if (!source_node) return;
    dest_node = std::make_unique<Node>(source_node->val);
    deep_copy_tree(dest_node->left, source_node->left);
    deep_copy_tree(dest_node->right, source_node->right);
}


int main()
{
    BinarySearchTree myBST1(5);
    BinarySearchTree myBST2 = myBST1; // copy constructor

    BinarySearchTree myBST3(4);
    myBST3 = myBST1; // copy assignment

    std::cout << "myBST3.empty() before move: " << myBST3.empty() << '\n';
    BinarySearchTree myBST4(std::move(myBST3)); // move constructor
    std::cout << "myBST3.empty() after move: " << myBST3.empty() << '\n';

    std::cout << "myBST4.empty() before move assignment: " << myBST4.empty() << '\n';
    myBST2 = std::move(myBST4); // move assignment
    std::cout << "myBST4.empty() after move assignment: " << myBST4.empty() << '\n';


    return 0;
}

Solution

  • The copy constructor default-initializes and then checks if other_tree is empty to avoid deep-copying it. But you already do that check in deep_copy_tree. Why not just initialize with that directly?

    BinarySearchTree::BinarySearchTree(const BinarySearchTree& other_tree) : tree_size{other_tree.tree_size} {
        std::cout << "Copy constructor\n";
        deep_copy_tree(root, other_tree.root);
    }
    

    To go further, I would make deep_copy_tree return instead of taking an out-parameter (and also drop the "_tree"; it's already in a "Tree" class).

    std::unique_ptr<Node> BinarySearchTree::deep_copy(const std::unique_ptr<Node>& source_node) {
        if (!source_node) return nullptr;
        auto dest_node = std::make_unique<Node>(source_node->val);
        dest_node->left = deep_copy(source_node->left);
        dest_node->right = deep_copy(source_node->right);
        return dest_node;
    }
    

    That way you can initialize root in the initializer list as well.

    BinarySearchTree::BinarySearchTree(const BinarySearchTree& other_tree) : root(deep_copy_tree(other_tree.root)), tree_size{other_tree.tree_size} {
        std::cout << "Copy constructor\n";
    }
    

    In the move constructor, you don't need to use std::exchange. In fact, for root, using std::move(other_tree.root) does the same (moved-from unique_ptrs are nullptrs).

    In the copy assignment operator, you probably want to check for self-assignment.

    if (this != &other_tree)
    

    You also don't need the clear in either assignment operators, since assigning to the unique_ptr effectively destroys the value it was holding.