When designing an architecture you might come across a need to create a collection of a template class. At first it'll sound easy, but soon you'll find that it's almost impossible to archive. At the end of this article, you'll be able to archive the impossible.
Previous article in series: Substitution Failure is Not an Error – SFINAE
The Problem
A little reminder from a previous article [templates infinity theory - part 1] for the shape class (with an additional shape name):
template <typename ...Properties> // Variadic Template
class shape : virtual public Properties... { // Multiple properties inheritances, the virtual will be more significant in the future
public:
explicit shape(std::string name) : name(std::move(name)) {}
virtual ~shape() = default;
virtual void input_data() { (Properties::input_data(), ...); }
[[nodiscard]] virtual double area() const = 0;
[[nodiscard]] std::string get_name() const { return name; }
protected: std::string name;
};
After we designed our shape base class, we created some different shapes with different properties (this is a shortcut, for the full example please refer to the original article with this example):
class rectangle : public shape<width_shape_property, height_shape_property> {
public:
rectangle() : shape("Rectangle") {}
void input_data() override { std::cout << "Input " << name << ":" << std::endl; shape::input_data(); }
[[nodiscard]] double area() const override { return width * height; }
};
class triangle : public shape<width_shape_property, height_shape_property> {
public:
triangle() : shape("Triangle") {}
void input_data() override { std::cout << "Input " << name << ":" << std::endl; shape::input_data(); }
[[nodiscard]] double area() const override { return (width * height) / 2; }
};
class circle : public shape<radios_shape_property> {
public:
circle() : shape("Circle") {}
void input_data() override { std::cout << "Input " << name << ":" << std::endl; shape::input_data(); }
[[nodiscard]] double area() const override { return M_PI * radios * radios; }
};
Now we might want to create a collection to contain different shapes, they all inherit from the same class after all:
int main() {
std::vector<std::shared_ptr<shape>> shapes;
return EXIT_SUCCESS;
}
And... We got an error, which is basically saying "Which shape vector do you want to create?". Well, it didn't go so easy. To understand it, we need to understand the issue.
shape
is not a class, it's a template class. Every specialization will be an unrelated, independent class, which means we can't contain them under the same pointer type. Moreover different specializations might not implement the same interface:
template <typename T>
class base {
public:
void func() {}
};
template<>
class base<int> {
public:
void my_func() {} // No func here!
}
template<>
class base<double> {
public:
double func; // func here is a class member variable
}
Now, with the understanding that we need to create a shared interface, which will hold for every specialization we'll create, it is the right time to build our solution.
The Solution
Note: In 2013 Sean Parent published a presentation that this solution is based on: Inheritance Is The Base Class of Evil.
Let's start with easy steps before we get to the most generic solution. We understood that we can't create a shared template interface with a guarantee that it'll really stay a "shared". So let's separate our base template class from the actual interface:
class shape_interface {
public:
virtual ~shape_interface() = default;
virtual void input_data() = 0;
[[nodiscard]] virtual double area() const = 0;
};
class shape_property {
public:
virtual void input_data() = 0;
};
template <typename T>
concept ShapeProperty = std::is_base_of_v<shape_property, T>;
template <ShapeProperty ...Properties>
class shape : public shape_interface, virtual public Properties... {
public:
explicit shape(std::string name) : name(std::move(name)) {}
virtual ~shape() = default;
void input_data() override { (Properties::input_data(), ...); };
[[nodiscard]] std::string get_name() const { return name; }
protected: std::string name;
};
With this shape_interface
we can now create a vector which will hold our shapes:
int main() {
std::vector<std::shared_ptr<shape_interface>> vec;
vec.emplace_back(std::make_shared<rectangle>());
vec.emplace_back(std::make_shared<triangle>());
vec.emplace_back(std::make_shared<circle>());
return EXIT_SUCCESS;
}
Hooray! It worked just like any other interface! Let's go home and drink a cold beer!
When you woke up in the morning with a little headache, you noticed a new email from work:
Hi there, I'm Bob, a coworker of you.
I saw your solution for the shapes, and I loved it!
However, I just found a library which offers a lot of shapes with a similar interface. It has the same methods with some extra, so I thought it might be nice thing to include their shapes as well.
One issue: I can't see their source code, so I can't modify their interface. Any ideas how to solve this issue?
Cheers, Bob.
The first thought that came to your mind is: Did he have to use "Cheers"? The second one is why the hell did he find this library?? So you came back to your work and started to think about the new situation.
You have a new class that implements the same functionality you required in your interface, but it simply uses another one:
class independent_legal_shape : another_interface {
public:
independent_legal_shape() = default;
void input_data() {
std::cout << "No input needed for independent_legal_shape." << std::endl;
}
[[nodiscard]] double area() const {
return 50;
}
};
int main() {
std::vector<std::shared_ptr<shape_interface>> vec;
vec.emplace_back(std::make_shared<rectangle>());
vec.emplace_back(std::make_shared<triangle>());
vec.emplace_back(std::make_shared<circle>());
// vec.emplace_back(std::make_shared<independent_legal_shape>()); // Obviously that won't compile
return EXIT_SUCCESS;
}
Suddenly, out of the blue, an idea came to your mind: What about creating a container which will hold any type and just call its methods of the desired interface. Something like that:
template <typename T>
class shape_interface_implementation : public shape_interface {
T& m_impl_obj; // Contain the target specialized class instance
public:
explicit shape_interface_implementation(T& impl_obj) : m_impl_obj(impl_obj) {}
// Interface should be fully implemented here
// region [Interface Implementation Begin]
void input_data() override { return m_impl_obj.input_data(); };
[[nodiscard]] double area() const override { return m_impl_obj.area(); };
// endregion [Interface Implementation End]
};
Now your main will look like that:
int main() {
std::vector<std::shared_ptr<shape_interface>> vec;
rectangle r; triangle t; circle c; independent_legal_shape my_shape;
vec.emplace_back(std::make_shared<shape_interface_implementation<rectangle>>(r));
vec.emplace_back(std::make_shared<shape_interface_implementation<triangle>>(t));
vec.emplace_back(std::make_shared<shape_interface_implementation<circle>>(c));
vec.emplace_back(std::make_shared<shape_interface_implementation<independent_legal_shape>>(my_shape));
return EXIT_SUCCESS;
}
It works, but something feels a little bit fishy here. It looks a lot more complicated than it was before, and something tells you that it can be simplified. A little search around the web, and you find a short video of Sean Parent: Inheritance Is The Base Class of Evil. There he presented a wrapper class which holds the pointer to the interface inside it. With little modifications to hold a direct access to the objects of the wrapper, you get the following wrapper class:
class template_base_shape_container {
public:
template <typename T>
explicit template_base_shape_container(T &obj) : m_obj(std::make_shared<interface_implementation<T>>(obj)) {}
// Get a pointer to the interface implementation
std::shared_ptr<shape_interface> get() { return m_obj; }
private:
template <typename T>
class shape_interface_implementation : public shape_interface { // The exact same interface_implementation as before
T& m_impl_obj; // Contain the target specialized class instance
public:
explicit shape_interface_implementation(T& impl_obj) : m_impl_obj(impl_obj) {}
// Interface should be fully implemented here
// region [Interface Implementation Begin]
void input_data() override { return m_impl_obj.input_data(); };
[[nodiscard]] double area() const override { return m_impl_obj.area(); };
// endregion [Interface Implementation End]
};
std::shared_ptr<shape_interface> m_obj; // Inner pointer
};
int main() {
std::vector<template_base_shape_container> shapes;
rectangle r; triangle t; circle c; independent_legal_shape my_shape;
shapes.emplace_back(r);
shapes.emplace_back(t);
shapes.emplace_back(c);
shapes.emplace_back(my_shape);
return EXIT_SUCCESS;
}
Looks even better than the simple shared interface. The only change is the access which now looks like that:
for (auto &shape : shapes) {
shape.get()->input_data();
std::cout << shape.get()->area() << std::endl;
}
Another day past with a success, but this time you don't risk yourself with a beer. You fall asleep wishing to wake up without any new work emails. When you wake up, surprisingly no new emails. You come to another work day, happy that you completely solved the issue, but then it hit you. You look at the nice code you left the other day, and you just can't believe it. New 10 wrapping classes which look exactly the same, with a single change: The Interface [Coming Soon at IMDb]. Let's make the container a generic container!
// A generic interface implementation
template <typename Interface, typename T>
class interface_implementation : public Interface {
public:
explicit interface_implementation(T& impl_obj) : m_impl_obj(impl_obj) {}
// Interface should be fully implemented here
// region [Interface Implementation Begin]
// endregion [Interface Implementation End]
private:
T& m_impl_obj; // Contain the target specialized class instance
};
// A generic template base container to match any future interface
template <typename Interface>
class template_base_container {
public:
template <typename T>
explicit template_base_container(T &obj) : m_obj(std::make_shared<interface_implementation<Interface, T>>(obj)) {
static_assert(std::is_base_of_v<Interface, interface_implementation<Interface, T>>, "In template_base_container interface_implementation<Interface, T> should inherit from Interface template param.");
}
// Get a pointer to the interface implementation
std::shared_ptr<Interface> get() { return m_obj; }
private:
std::shared_ptr<Interface> m_obj;
};
And for the interface specialization:
// Interface implementation specialization for shape_interface
template <typename T>
class interface_implementation<shape_interface, T> : public shape_interface {
public:
explicit interface_implementation(T& impl_obj) : m_impl_obj(impl_obj) {}
// Interface should be fully implemented here
// region [Interface Implementation Begin]
void input_data() override { return m_impl_obj.input_data(); };
[[nodiscard]] double area() const override { return m_impl_obj.area(); };
// endregion [Interface Implementation End]
private:
T& m_impl_obj; // Contain the target specialized class instance
};
Our main stays without a change:
int main() {
std::vector<template_base_container<shape_interface>> shapes;
rectangle r; triangle t; circle c; independent_legal_shape my_shape;
shapes.emplace_back(r);
shapes.emplace_back(t);
shapes.emplace_back(c);
shapes.emplace_back(my_shape);
for (auto &shape : shapes) {
shape.get()->input_data();
std::cout << shape.get()->area() << std::endl;
}
return EXIT_SUCCESS;
}
Conclusion
Templates and inheritances combinations are a huge topic with almost infinite possibilities in C++. This is the time to thank for Sean Parent for his "Inheritance Is The Base Class of Evil" amazing talk, that without it this article wouldn't be made.
Feel free to share your experience with templates & inheritances here, and if you see any improvements that can made to make this container even more generic or written with less code duplications (e.g. interface_implementation have a code duplication for every specialization).
Updated repository for template_base_container: common_template_base
This article originally published on my personal blog: C++ Senioreas
Top comments (0)