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.