Thomas Sablik
Thomas Sablik

Reputation: 16449

Clone vector of unique pointers with RTTI

I want to clone a vector of unique pointers using RTTI.

Currently, there is an abstract base class, Node, and derived classes, Element and TextNode. Element contains a vector of unique Node pointers.

I'm able to create an object of type Element and move it into the vector. I want to be able to clone an Element and push a copy into the vector, but I'm struggling with the copy constructor of Element.

Is this possible? How can I clone a unique pointer using RTTI? Is there a better approach for this problem?

#include <iostream>
#include <memory>
#include <string>
#include <vector>

struct Node {
    virtual ~Node() = default;
    virtual std::string toString() const = 0;
};

struct Element : Node {
    Element() = default;
    Element(const Element &element) {
        // clone children
        // for (const auto &child : element.children) children.push_back(std::make_unique</* get RTTI */>(child));
    }
    Element(Element &&) = default;
    std::string toString() const override {
        std::string str = "<Node>";
        for (const auto &child : children) str += child->toString();
        str += "</Node>";
        return str;
    }
    std::vector<std::unique_ptr<Node>> children;
};

struct TextNode : Node {
    std::string toString() const override { return "TextNode"; }
};

int main() {
    Element root;
    Element node;
    node.children.push_back(std::make_unique<TextNode>());
    // This copy doesn't work because I don't know how to implement the copy constructor
    root.children.push_back(std::make_unique<Element>(node));
    root.children.push_back(std::make_unique<Element>(std::move(node)));
    root.children.push_back(std::make_unique<TextNode>());
    
    std::cout << root.toString();
}

Actual output:

<Node><Node></Node><Node>TextNode</Node>TextNode</Node>

Expected output:

<Node><Node>TextNode</Node><Node>TextNode</Node>TextNode</Node>

Upvotes: 1

Views: 66

Answers (1)

Remy Lebeau
Remy Lebeau

Reputation: 596407

What you are asking for can't be done directly with RTTI, but it can be done indirectly by checking the class types manually. Since you have only a small number of classes to check, you can do that like this:

#include <iostream>
#include <memory>
#include <string>
#include <vector>

struct Node {
    virtual ~Node() = default;
    virtual std::string toString() const = 0;
};

struct Element : Node {
    Element() = default;

    Element(const Element &element) {
        // clone children
        for (const auto &child : element.children) {
            Node *n = child.get();
            if (Element *e = dynamic_cast<Element*>(n)) {
                children.push_back(std::make_unique<Element>(*e));
            }
            else if (TextNode *t = dynamic_cast<TextNode*>(n)) {
                children.push_back(std::make_unique<TextNode>(*t));
            }
        }
    }

    Element(Element &&) = default;

    std::string toString() const override {
        std::string str = "<Node>";
        for (const auto &child : children)
            str += child->toString();
        str += "</Node>";
        return str;
    }

    std::vector<std::unique_ptr<Node>> children;
};

struct TextNode : Node {
    std::string toString() const override { return "TextNode"; }
};

int main() {
    Element root;
    Element node;
    node.children.push_back(std::make_unique<TextNode>());
    root.children.push_back(std::make_unique<Element>(node));
    root.children.push_back(std::make_unique<Element>(std::move(node)));
    root.children.push_back(std::make_unique<TextNode>());
    
    std::cout << root.toString();
}

But, needless to say, this will get tedious and error-prone if you add more descendant classes later.

A better option is to add a virtual clone() method to Node, and then have Element and TextNode (and future descendants) override it to make copies of themselves, eg:

#include <iostream>
#include <memory>
#include <string>
#include <vector>

struct Node {
    virtual ~Node() = default;
    virtual std::string toString() const = 0;
    virtual std::unique_ptr<Node> clone() const = 0;
};

struct Element : Node {
    Element() = default;

    Element(const Element &element) {
        // clone children
        for (const auto &child : element.children)
            children.push_back(child->clone());
    }

    Element(Element &&) = default;

    std::string toString() const override {
        std::string str = "<Node>";
        for (const auto &child : children)
            str += child->toString();
        str += "</Node>";
        return str;
    }

    std::unique_ptr<Node> clone() const override {
        return std::make_unique<Element>(*this);
    }

    std::vector<std::unique_ptr<Node>> children;
};

struct TextNode : Node {
    std::string toString() const override { return "TextNode"; }

    std::unique_ptr<Node> clone() const override {
        return std::make_unique<TextNode>(*this);
    }
};

int main() {
    Element root;
    Element node;
    node.children.push_back(std::make_unique<TextNode>());
    root.children.push_back(std::make_unique<Element>(node));
    root.children.push_back(std::make_unique<Element>(std::move(node)));
    root.children.push_back(std::make_unique<TextNode>());
    
    std::cout << root.toString();
}

Upvotes: 2

Related Questions