C++ STL Containers of Unique Pointers to Polymorphic Class Template

To those who have never seen polymorphism, this will look like black magic. To those who don’t utilize the power of generic programming this will look like something from another world completely.

As an aside, if you like this article you may also like C++ :: Mapping User Input to Actions

Basically, the idea is to have a container, maybe an std::vector, of objects with different types, and each object will perform a set of tasks in a certain order. You may also want the tasks to be programmatically called with user input without conditionals. This will drastically reduce code base size and increase maintainability by adding a little complexity. For this tutorial, each code block is copy and paste compilable. Let’s begin.

You may know that you can create a container that holds a polymorphic type using pointers to the base type.

#include <iostream>
#include <vector>

class Base{
public:
    // Pure virtual function
    virtual void func() = 0;
    virtual ~Base() = default;
};

class Derived : public Base{
public:


    // override the pure virtual function in Base
    void func() override
    {
        std::cout << "Derived::func\n";
    }
};

int main(void)
{
    std::vector<Base*> vec_poly_ptr;
    vec_poly_ptr.push_back(new Derived{});

    // Call Base::func which propagates to Derived::func
    vec_poly_ptr.at(0)->func();
}

I hope you can see how we need to create a virtual function in our Base class for every derived class function in order to call it from our container. Now let’s do the same thing with std::unique_ptr using the classes from above.

#include <memory> // Add to top of previous code

// Replace previous codes `main()` with this
int main(void)
{
    std::vector<std::unique_ptr<Base>> vec_poly_ptr;
    vec_poly_ptr.push_back(std::make_unique<Derived>());

    // Call Base::func which propagates to Derived::func
    vec_poly_ptr.at(0)->func();
}

This is all pretty standard polymorphic code. But now, we want to make a single call to a member function of our derived object that will then call other member functions in any order we choose. This also alleviates the need to create a virtual function in the base class for every function in the derived class.

#include <iostream>
#include <vector>
#include <memory>
#include <unordered_map>

class Base{
public:
    // Pure virtual function
    virtual Base& next_step() = 0;
    virtual ~Base() = default;
};

class Derived : public Base{
public:
    // Alias for pointer to member function
    using ptr_to_mem_func = Derived& (Derived::*)();

    // Alias of unordered_map of <integer, pointer to member function>
    using map_of_mem_func = std::unordered_map<int, ptr_to_mem_func>;

    size_t step_num = 0;

    map_of_mem_func funcs = {
        {0, &Derived::func1},
        {1, &Derived::func2}
    };

    // member function exclusive to Derived
    Derived& func1()
    {
        std::cout << "Derived::func1\n";

        return *this;
    }

    // member function exclusive to Derived
    Derived& func2()
    {
        std::cout << "Derived::func2\n";

        return *this;
    }

    // override the pure virtual function in Base
    Derived& next_step() override
    {
        if (step_num < funcs.size())
        {
            (this->*funcs[step_num])();
            step_num++;
        }

        return *this;
    }
};

int main(void)
{
    std::vector<std::unique_ptr<Base>> vec_poly_ptr;
    vec_poly_ptr.push_back(std::make_unique<Derived>());

    vec_poly_ptr.at(0)
        ->next_step()
        .next_step()
        .next_step();
}

This is all well and good, and we could have used an std::string as a parameter to Base::next_step and an std::unordered_map<std::string, ptr_to_mem_func> to call each function from user input without conditionals (I’ll leave that as an exercise for you).

Did you notice anything funny with this design? Did you see that we need to override Base::next_step in every derived class? Don’t you think this would get a little redundant if we have a large number of derived types? I think we can do better.

#include <iostream>
#include <string>
#include <unordered_map>
#include <memory>

using String = std::string;

// Base class for polymorphic container type
class FruitType {
public:
    virtual FruitType& i_will(String do_something) = 0;
    virtual ~FruitType() = default;
};

// Derived class template wrapper for implementation classes
template <typename T>
class Fruit
    : public FruitType
{
public:

    // The implementation class object
    T the_fruit{};

    /*
        This will call a member function in the_fruit
        if the_fruit has defined a tasks variable of
        pointers to member function
     */
    Fruit& i_will(String do_something)
    {
        if (the_fruit.tasks.count(do_something))
            (the_fruit.*the_fruit.tasks[do_something])();

        return *this;
    }

};

// Implementation class
class Apple {
public:
    using Task = Apple& (Apple::*)();
    using Tasks = std::unordered_map<String, Task>;

    /*
        We must use a map to give the wrapper knowledge
        of this classes member functions
     */
    Tasks tasks = {
        {"slice", &Apple::slice},
        {"eat", &Apple::eat}
    };

    bool is_sliced = false;
    bool is_eaten = false;

    Apple& slice()
    {
        if (!is_sliced)
            std::cout << "Slicing the Apple\n";
        else
            std::cout << "The Apple is already sliced\n";

        is_sliced = true;

        return *this;
    }

    Apple& eat()
    {
        if (!is_eaten)
            std::cout << "Eating the Apple\n";
        else
            std::cout << "The Apple is already eaten\n";

        is_eaten = true;

        return *this;
    }
};

class Orange {
public:
    using Task = Orange& (Orange::*)();
    using Tasks = std::unordered_map<String, Task>;

    Tasks tasks = {
        {"peel", &Orange::peel},
        {"eat", &Orange::eat}
    };

    bool is_peeled = false;
    bool is_eaten = false;

    Orange& peel()
    {
        if (!is_peeled)
            std::cout << "Peeling the Orange\n";
        else
            std::cout << "The Orange is already peeled\n";

        is_peeled = true;

        return *this;
    }

    Orange& eat()
    {
        if (!is_eaten)
            std::cout << "Eating the Orange\n";
        else
            std::cout << "The Orange is already eaten\n";

        is_eaten = true;

        return *this;
    }
};

/*
    This creates a new instance of type Fruit<T>
    and returns an std::unique_ptr<Fruit<T>>
 */
template <typename T>
std::unique_ptr<FruitType> pick()
{
    return std::make_unique<Fruit<T>>();
}

int main()
{
    using Fruit_Stand = std::unordered_map<String, std::unique_ptr<FruitType>(*)()>;
    using My_Basket = std::unordered_map<String, std::unique_ptr<FruitType>>;

    /*
        This is a mapping of different types
        the pick function can produce
     */
    Fruit_Stand fs = {
        {"apple", &pick<Apple>},
        {"orange", &pick<Orange>}
    };

    /*
        Here we can control the lifetime of our
        derived objects dynamically/programmatically
     */
    My_Basket mb;
    mb.emplace("apple", fs.at("apple")());
    mb.emplace("orange", fs.at("orange")());

    /*
        As you can see, we don't need knowledge of
        each member function of every derived class.
        This totally eliminates the need for conditionals
     */
    mb.at("apple")->i_will("slice").i_will("slice");
    mb.at("apple")->i_will("eat").i_will("eat");
    mb.at("orange")->i_will("peel").i_will("peel");
    mb.at("orange")->i_will("eat").i_will("eat");

    // These two calls do nothing
    mb.at("orange")->i_will("slice").i_will("slice");
    mb.at("apple")->i_will("peel").i_will("peel");
}

To keep with the style of allowing you to copy and paste the code directly from this page, I have given you the entirety of the code for quick compilation and execution without the need to find all the pieces interspersed throughout this tutorial.

You may ask “Why?” or “What does this do that using conditionals doesn’t do?”. My answer would be, “Maintainability, data segregation, extensibility”. As you can see, you can create a long-lived object that is easily moved around from place to place since it is managed by an std::unique_ptr (You could also use an std::shared_ptr). This allows you to create wildly different objects without a base class full of virtual functions. You can keep all of the logic for each class within the boundaries of the class and expose a simple interface to the client through the mappings.