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

…衆ロ難τιáo~ 提交于 2020-12-27 07:46:41

问题


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


回答1:


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.



来源:https://stackoverflow.com/questions/26199126/clarification-about-sean-parents-talk-inheritance-is-the-base-class-of-evil

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!