c++polymorphismsmart-pointers

How to prevent code heavily relying on polymorphism from being littered with `make_shared` all over the place?


I am writing a UI framework for a microcontroller, in which I want to do a hierarchical menu system.

For that in particular I have the following classes:

class MenuNode {
public:
    MenuNode(const std::string t, const UI::Image* img): title{t}, icon{img} {}
    virtual ~MenuNode() = default;

    const std::string title;
    const UI::Image * icon;
    virtual void execute(MenuNavigator * host) const { ESP_LOGE("MenuNode", "Did you forget to override execute()?"); }
    virtual bool is_submenu() const { return false; }
};


class ListMenuNode: public MenuPresentable, public MenuNode,  public UI::ListView {
public:
    ListMenuNode(const std::string title, const std::vector<std::shared_ptr<MenuNode>>& items, const UI::Image* icon = nullptr): 
        subnodes(items),
        UI::ListView(EGRectZero, {}), MenuNode(title, icon) {
    }

    void execute(MenuNavigator * host) const override {
        host->push(std::make_shared<ListMenuNode>(*this));
    }

    void on_presented() override {
        // update frames of things after push has set our frame correctly
        ...
    }

    void on_key_pressed(VirtualKey k, MenuNavigator* host) override {
        if(k == RVK_CURS_DOWN) down();
        else if(k == RVK_CURS_UP) up();
        else if(k == RVK_CURS_ENTER) {
            subnodes[selection]->execute(host);
        }
        else if(k == RVK_CURS_LEFT) {
            host->pop();
        }
    }
protected:
    const std::vector<std::shared_ptr<MenuNode>> subnodes;
};

In here, I have to make subnodes a vector of shared_ptr<MenuNode> instead of directly MenuNode — otherwise upon selection of an item, only MenuNode::execute() ever gets called instead of the descendant class override of this method.

I am fine with that for the most part, however, this means a declaration of the menu looks like the following

ListMenuNode(
 "Settings",
    {
        std::make_shared<MenuNode>("Item 1", &icn_wifi),
        std::make_shared<ListMenuNode>(ListMenuNode("Drill Down", {
            std::make_shared<MenuNode>("Drill Item 1", &icn_cd),
            std::make_shared<MenuNode>("Drill Item 2", &icn_bt),
            std::make_shared<MenuNode>("Drill Item 3", &icn_radio),
        }, &icn_about))
    }
)

Which looks very unclean due to repetitive calls to make_shared.

Is there some way to make the pointer creation implicit, so that I can just write something like the following instead?

ListMenuNode(
 "Settings",
    {
        MenuNode("Item 1", &icn_wifi),
        ListMenuNode("Drill Down", {
            MenuNode("Drill Item 1", &icn_cd),
            MenuNode("Drill Item 2", &icn_bt),
            MenuNode("Drill Item 3", &icn_radio),
        }, &icn_about)
    }
)

One way I thought of was using #define's for each class name, but that sounds not very clean to me.


Solution

  • Not sure it is a good idea, but, if your types are movable, you might use variadic template constructor.

    using std::tuple instead of std::vector:

    template <typename... Ts>
    ListMenuNode(const std::string title,
                 std::tuple<Ts...>&& items,
                 const UI::Image* icon = nullptr) :
        subnodes(std::apply([](auto&&... args) -> std::vector<std::shared_ptr<MenuNode>>
                            {
                                 return { std::make_shared<Ts>(std::move(args))... };
                             }, std::move(items)),
        UI::ListView(EGRectZero, {}),
        MenuNode(title, icon)
    {
    }
    

    With call similar to

    ListMenuNode(
     "Settings",
        std::tuple{
            MenuNode("Item 1", &icn_wifi),
            ListMenuNode("Drill Down", std::tuple{
                MenuNode("Drill Item 1", &icn_cd),
                MenuNode("Drill Item 2", &icn_bt),
                MenuNode("Drill Item 3", &icn_radio),
            }, &icn_about)
        }
    )