Подтвердить что ты не робот

Разъяснение о Шоне Родительском разговоре "Наследование - это базовый класс зла"

Шон Родительский разговор, Наследование является базовым классом зла, говорит, что полиморфизм не является свойством типа, а скорее является свойством как он используется. В качестве правила большого пальца не используйте наследование для реализации интерфейсов. Среди многих преимуществ этого является девиртуализация классов, которые имеют виртуальные функции только потому, что они реализуют интерфейс. Вот пример:

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;
};

Здесь вместо UseDrawable требуется mDraw быть Drawable*, вы можете использовать его класс стираемого типа, который может обертываться вокруг любого класса, реализующего элемент с именем draw. Итак, что-то вроде boost::type_erasure::any с соответствующим определением. Таким образом, DrawA не нужно наследовать от Drawable - полиморфизм был действительно UseDrawable требованием и не действительно свойством DrawA.

Я пытаюсь реорганизовать некоторый код, следуя этому принципу. У меня есть абстрактный класс ModelInterface и два конкретных класса ModelA и ModelB, наследующие от ModelInterface. Следуя совету Шона, имеет смысл не форсировать ModelA и ModelB в иерархию наследования, а просто использовать стирание типа в местах, где требуется класс, удовлетворяющий понятию, моделируемому ModelInterface.

Теперь моя проблема в том, что большинство мест в моем коде, которые в настоящее время используют ModelInterface, также делают это, создавая соответствующий объект на основе файла конфигурации времени исполнения. В настоящее время factory имеет new соответствующий объект и возвращает ModelInterface*. Если я реорганизую код для использования стираемой стили (скажем что-то вроде boost::type_erasure::any<implement ModelInterface>) в этих местах в коде, как мне построить такие объекты во время выполнения? Будут ли ModelA и ModelB все еще нужны классы с поддержкой RTTI? Или я могу factory -строить и использовать их без RTTI-информации?

(С RTTI я могу иметь абстрактный класс, скажем FactoryConstructible, и использовать dynamic_cast<void*> для получения окончательного типа.)

4b9b3361

Ответ 1

Стирание стилей 101:

Шаг 1: создайте обычный (или полурегулярный тип только для перемещения), который скрывает детали.

struct exposed_type;

Этот класс предоставляет концепции, которые вы хотите поддерживать. Копирование, перемещение, уничтожение, равен, общий порядок, хеш и/или любые специальные концепции, которые необходимо поддерживать.

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
};

Многие из этих концепций могут быть грубо сопоставлены с помощью метода чистого виртуального интерфейса в вашем текущем решении на основе наследования.

Создайте не виртуальные методы в вашем типе Regular, который выражает понятия. Копировать/назначить для копирования и т.д.

Шаг 2: Напишите помощник стирания типа.

struct internal_interface;

Здесь у вас есть чистые виртуальные интерфейсы. clone() для копирования и т.д.

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;
};

Сохраните интеллектуальный указатель 1 для этого в вашем стандартном типе выше.

struct exposed_type {
  std::unique_ptr<internal_interface> upImpl;

Переслать регулярные методы помощнику. Например:

exposed_type::exposed_type( exposed_type const& o ):
  upImpl( o.upImpl?o.upImpl->clone():nullptr )
{}
exposed_type::exposed_type( exposed_type&& o )=default;

Шаг 3: напишите реализацию стирания типа. Это класс template, который хранит T и наследует от помощника, и переводит интерфейс в T. Используйте бесплатные функции (вроде как std::begin), которые используют методы в реализации по умолчанию, если не была найдена свободная функция adl.

// 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
  }
};

Шаг 4: добавьте конструктор к вашему регулярному типу, который принимает T и создает из него реализацию стирания типа, и наполняет его своим умным указателем на помощника.

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)} )
{}

После всей этой работы у вас теперь есть неинтрузивная полиморфная система с регулярным (или полурегулярным) типом значения.

Ваши функции factory возвращают правильный тип.

Посмотрите на примеры реализации std::function, чтобы убедиться, что это сделано полностью.


1 как уникальные, так и общие - хорошие варианты, в зависимости от того, хотите ли вы хранить неизменяемые/копировать данные записи или вручную клонировать.