Sean Parent's talk, Inheritance is the base class of evil, says that polymorphism is not a property of the type, but rather a property of how it is used. As a thumb rule, don't use inheritance to implement interfaces. Among the many benefits of this is the devirtualization of classes which have virtual functions only because they were implementing an interface. Here's an example :
class Drawable
{
public:
virtual void draw() = 0;
};
class DrawA : public Drawable
{
public:
void draw() override{//do something}
};
class UseDrawable
{
public:
void do(){mDraw->draw();}
Drawable* mDraw;
};
Here, instead of UseDrawable
requiring mDraw
to be a Drawable*
, you could have it use a type-erased class which can wrap around any class implementing a member called draw
. So, something like a boost::type_erasure::any
with the appropriate definition. That way, DrawA
doesn't need to inherit from Drawable
- the polymorphism was really UseDrawable
s requirement and not really a property of DrawA
.
I am trying to refactor some code following this principle. I have an abstract class ModelInterface
and two concrete classes ModelA
and ModelB
inheriting from ModelInterface
. Following Sean's advice, it makes sense not to force ModelA
and ModelB
into the inheritance hierarchy and simply use type-erasure at locations which require a class satisfying the concept modelled by ModelInterface
.
Now, my problem is that most places in my code which currently use a ModelInterface
also do so by constructing an appropriate object based on a runtime configuration file. Currently, the factory would new
an appropriate object and return a ModelInterface*
. If I refactor the code to use a type-erased concept(say something like boost::type_erasure::any<implement ModelInterface>
) at these locations in code, how do I construct such objects at runtime? Will ModelA
and ModelB
still need to be RTTI-enabled classes? Or can I factory-construct and use them without RTTI info somehow?
(With RTTI, I can have an abstract class, say FactoryConstructible
, and use dynamic_cast<void*>
to get the final type.)
Multiple Inheritance in C++ Multiple Inheritance is a feature of C++ where a class can inherit from more than one classes. The constructors of inherited classes are called in the same order in which they are inherited.
Inheritance enables you to create new classes that reuse, extend, and modify the behavior defined in other classes. The class whose members are inherited is called the base class, and the class that inherits those members is called the derived class. A derived class can have only one direct base class.
Type erasure 101:
Step 1: make a regular (or semi-regular move-only) type that hides the detail.
struct exposed_type;
This class exposes the concepts you want to support. Copy, move, destroy, equals, total order, hash, and/or whatever custom concepts you need to support.
struct exposed_type {
exposed_type(exposed_type const&);
exposed_type(exposed_type&&);
friend bool operator<(exposed_type const&, exposed_type const&);
friend std::size_t hash(exposed_type const&);
// etc
};
Many of these concepts can be roughly mapped from a pure virtual interface method in your current inheritance based solution.
Create non-virtual methods in your Regular type that expresses the concepts. Copy/assign for copy, etc.
Step 2: Write a type erasure helper.
struct internal_interface;
Here you have pure virtual interfaces. clone()
for copy,etc.
struct internal_interface {
virtual ~internal_interface() {}
virtual internal_interface* clone() const = 0;
virtual int cmp( internal_interface const& o ) const = 0;
virtual std::size_t get_hash() const = 0;
// etc
virtual std::type_info const* my_type_info() const = 0;
};
Store a smart pointer1 to this in your Regular type above.
struct exposed_type {
std::unique_ptr<internal_interface> upImpl;
Forward the regular methods to the helper. For example:
exposed_type::exposed_type( exposed_type const& o ):
upImpl( o.upImpl?o.upImpl->clone():nullptr )
{}
exposed_type::exposed_type( exposed_type&& o )=default;
Step 3: write a type erasure implementation. This is a template
class that stores a T
and inherits from the helper, and forwards the interface to the T
. Use free functions (sort of like std::begin
) that uses methods in the default implementation if no adl free function was found.
// used if ADL does not find a hash:
template<class T>
std::size_t hash( T const& t ) {
return std::hash<T>{}(t);
}
template<class T>
struct internal_impl:internal_interface {
T t;
virtual ~internal_impl() {}
virtual internal_impl* clone() const {
return new internal_impl{t};
}
virtual int cmp( internal_interface const& o ) const {
if (auto* po = dynamic_cast<internal_interface const*>(&o))
{
if (t < *po) return -1;
if (*po < t) return 1;
return 0;
}
if (my_type_info()->before(*o.my_type_info()) return -1;
if (o.my_type_info()->before(*my_type_info()) return 1;
ASSERT(FALSE);
return 0;
}
virtual std::size_t get_hash() const {
return hash(t);
}
// etc
std::type_info const* my_type_info() const {
return std::addressof( typeid(T) ); // note, static type, not dynamic
}
};
Step 4: add a constructor to your regular type that takes a T
and constructs a type erasure implementation from it, and stuffs that in its smart pointer to the helper.
template<class T,
// SFINAE block using this ctor as a copy/move ctor:
std::enable_if_t<!std::is_same<exposed_type, std::decay_t<T>>::value, int>* =nullptr
>
exposed_type( T&& t ):
upImpl( new internal_impl<std::decay_t<T>>{std::forward<T>(t)} )
{}
After all this work, you now have non-intrusive polymorphic system with a regular (or semi-regular) value type.
Your factory functions return the regular type.
Look into sample implementations of std::function
to see this done fully.
1 both unique and shared are good choices, depending on if you want to store immutable/copy on write data, or manually clone.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With