Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clarification about Sean Parent's talk "Inheritance is the base class of evil"

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 UseDrawables 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.)

like image 512
Pradhan Avatar asked Oct 05 '14 02:10

Pradhan


People also ask

In which kind of inheritance A class can be derived using more than one parent class?

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.

Can classes inherit from the same base class?

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.


1 Answers

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.

like image 99
Yakk - Adam Nevraumont Avatar answered Oct 05 '22 02:10

Yakk - Adam Nevraumont