Problem
We often wish to collect data into containers in order to manage large numbers of things. To put it less correctly but more clearly, we want collections of objects that represent a family. The reason this is wrong is because fundamental types aren't often thought of as objects, and really we aren't talking about families, rather things to be processed a certain way. Why this distinction matters, I hope, will become clear.Base Class Solution
The almost reflexive solution is to derive everything from a common base class. Ideally this is an interface class that is pure virtual. There are a number of problems with this. Many experts, and I also agree, call this the most abused feature of C++.Fundamental types, and STL types do not inherit from anything requiring they be wrapped. Often objects bridge responsibilities requiring either multiple interfaces, or overly broad interfaces (kitchen sink classes).
Classes often are shoehorned into an interface, the famous birds analogy. When ostriches and emus refuse to fly, are they no longer birds?
Inheritance is often not a good solution.
Internal Polymorphism
Classes could be internally polymorphic. They could rely on internal flags to determine what kind of thing they are today, and this one, amorphous blob class can, internally, be whatever it wants. This is too horrific to consider further.Adaptor and Decorator Classes
We could wrap the class. How much do we wrap? Do we need every function adapted? Doesn't that start to bring back the same problem? Wrapping can also be tedious as it is difficult to automate. What if a class needs to behave as if it has functionality, but it doesn't. What if they have different memory sizes? We are getting close, but aren't quite there.Solution
Wrapping data has some definite benefits. We don't need to modify the classes. They can be written in a way that best expresses their use without imposing design from outside. But how do we solve the wrapping problems. We can use Concepts, and non-member functions.Concepts
Concepts are a generic programming term that describes operations on a type. It's like an interface class, but there is no inheritance and as of now, no compiler support, but that's OK. We can still use them. A Concept is really just a generic programmer telling programmers who use his stuff what functions and signatures are required. We've been using these all along. When we increment an iterator, there is no base class that defines that functionality. It's part of the documentation. Therefore, we can pass iterators into generic algorithms that expect that functionality.Non-Member Functions Increase Encapsulation
I know. This is just wrong! Wrong, wrong, wrong, wrong, wrong, wrong, wrong!Except it's not.
The reason we encapsulate things is to protect us against change. If I directly manipulate the data in a struct, that would not be well encapsulated. On the opposite extreme, it's hard to get better encapsulation than objects who's entire functionality resides in its constructor and destructor.
Encapsulation is measured by the number of functions that have to be changed if we change the implementation of our class. A class with n functions is more encapsulated than a class with n+1 functions. Non-member, non-friend functions do not increase n.
Here's an even bigger one. By using non-member functions, we effectively have polymorphism via function overloading. Rather than object->Draw(), and needing VTables, we can Draw(object) and leave it to compile time. This makes the problem much easier to solve.
Really, The Solution
Always start by writing the code you wish to write.class MyClass { }; void Draw(const MyClass& mc, ostream& out, size_t position) { out << "MyClass" << endl; } int main() { Document document; document.reserve(5); document.emplace_back(0); document.emplace_back(string("Hello")); document.emplace_back(document); document.emplace_back(MyClass()); reverse(begin(document), end(document)); reverse(begin(document), end(document)); Draw(document, cout, 0); return 0; }We have a collection of things that have no relation whatsoever to one another, yet we are adding them to a common container. How does this work?
Here's the whole thing.
#pragma once #include <iostream> #include <vector> #include <string> #include <memory> template <typename T> void Draw(const T& x, std::ostream& out, size_t position) out << std::string(position, ' ') << x << std::endl; } class DrawConcept { public: template <typename T> DrawConcept(const T& x) : m_object(new model<T>(x)) { std::cout << "Ctor" << std::endl; } DrawConcept(const DrawConcept& x) : m_object(x.m_object->copy_()) { std::cout << "Copy" << std::endl; } DrawConcept(DrawConcept&& x) : m_object(std::move(x.m_object)) {;} DrawConcept& operator=(DrawConcept x) { m_object = std::move(x.m_object); return *this; } friend void Draw(const DrawConcept& x, std::ostream& out, size_t position) { x.m_object->draw_(out, position); } private: struct concept_t { virtual ~concept_t() {;} virtual concept_t* copy_() = 0; virtual void draw_(std::ostream& out, size_t position) const = 0; }; template <typename T> struct model : concept_t { model(const T& x) : m_data(x) {;} concept_t* copy_() { return new model(*this); } void draw_(std::ostream& out, size_t position) const { Draw(m_data, out, position); } T m_data; }; std::unique_ptr<concept_t> m_object; }; typedef std::vector<DrawConcept> Document; void Draw(const Document& x, std::ostream& out, size_t position) { out << std::string(position, ' ') << "<document>"<< std::endl; for(auto& e : x) Draw(e, out, position + 2); out << std::string(position, ' ') << "</document>"<< std::endl; }To see how it works, let's take it apart.
struct concept_t { virtual ~concept_t() {;} virtual concept_t* copy_() = 0; virtual void draw_(std::ostream& out, size_t position) const = 0; };This is just a simple interface class. Note the copy_(). This is to ensure we get a copy of the actual derived class, not the base class. It's also written as clone().
The derived class is next.
template <typename T> struct model : concept_t { model(const T& x) : m_data(x) {;} concept_t* copy_() { return new model(*this); } void draw_(std::ostream& out, size_t position) const { Draw(m_data, out, position); } T m_data; };This is the wrapper around the draw-able object. By deriving from concept_t we get an object that we can hold type agnostic. By templatizing the class, we can hold anything. Copy returns a derived class copy, but the real trick is the draw function. Here we take the virtual member draw and make it a non-member draw, allowing compile-time polymorphism.
There's just one more piece.
template <typename T> DrawConcept(const T& x) : m_object(new model<T>(x)) { std::cout << "Ctor" << std::endl; }Even though the constructor is templatized, this is not a template class. That's important, because if it was it would be impossible to add to containers.
That's it.
The template function at the top is optional. It provides a common way to draw, but can be overloaded if an object has a different way to draw such as MyClass above.
The rest is just copy and assignment code common to most classes.
Conclusion
There are alternatives to typical inheritance. By using concept based polymorphism, you can leave your class interface alone to be the best representation of your abstraction and not a collection of externally forced design. Returning to those birds that refuse to fly, simply don't add them to a fly concept. If you do, rather than have a silent error running in your game, the compiler will squawk (pun intended) and the error gets fixed.References
Sean Parent's excellent talkAdobe Run Time Concepts