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\'
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.