Разъяснение по поводу выступления Шона Пэрента. Наследование — это базовый класс зла.

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

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

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


person Pradhan    schedule 05.10.2014    source источник
comment
Прошу прощения за многословный вопрос. Может ли близкий избиратель попросить разъяснения, пожалуйста?   -  person Pradhan    schedule 05.10.2014
comment
Я думаю, что, возможно, есть инстинкт, что если ваш вопрос не имеет четкого примера кода... но кажется философским и ссылается на чей-то разговор... то это подозрительно. Можете ли вы переформулировать этот вопрос, чтобы мне не нужно было переходить по ссылке и смотреть выступление, чтобы понять его? Сузьте и сфокусируйте вопрос. Тенденция здесь такова, что много людей, которым есть о чем поговорить, поэтому нужно, чтобы их направляли и просили, чтобы спрашивающие были очень конкретными.   -  person HostileFork says dont trust SE    schedule 05.10.2014
comment
Может ли избиратель попросить разъяснений? Конечно. Ваш вопрос имеет тенденцию быть либо "слишком широким", либо "основанным на мнении".   -  person πάντα ῥεῖ    schedule 05.10.2014
comment
@HostileFork Имеет смысл. Позвольте мне попытаться добавить пример кода, иллюстрирующий те моменты, о которых я говорю. Спасибо!   -  person Pradhan    schedule 05.10.2014
comment
ИМХО, boost::any для этой цели не подходит (по крайней мере, как интерфейс). Я думаю, вы должны вернуть некоторый Model из вашей фабрики, в которой хранится некий объект с стертым типом, выполняющий концепцию ModelInterface. Затем Model похож на Document, который использует Шон Пэрент (хотя он хранит не коллекцию, а один объект).   -  person dyp    schedule 05.10.2014
comment
как создавать такие объекты во время выполнения Точно так же, как любая фабрика создает объекты разных типов во время выполнения. Разница в том, что вы оборачиваете этот объект в объект Model и возвращаете объект Model по значению.   -  person dyp    schedule 05.10.2014


Ответы (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
};

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

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

Шаг 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 на this в обычном типе выше.

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

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

Ваши фабричные функции возвращают обычный тип.

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


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

person Yakk - Adam Nevraumont    schedule 05.10.2014
comment
добавьте конструктор к вашему обычному типу, который принимает T Возможно, было бы полезно предоставить бесплатную функцию template<class T, class... Args> Wrapper emplace_wrap(Args&&...);, чтобы тип мог быть помещен в класс реализации стирания типа без перемещения/копирования. - person dyp; 05.10.2014
comment
@dyp Это и тег emplace, который вы можете передать конструктору. Но это покрывается стиранием типа 102: на этот раз его переадресация. - person Yakk - Adam Nevraumont; 05.10.2014
comment
Где я могу прочитать больше об этих методах, упомянутых вещах, таких как emplace, perfect forwarding и type erase 102 и т. д. - person meguli; 15.11.2017
comment
@meguli Я много писал о SO-документации, но SO-документация была удалена. - person Yakk - Adam Nevraumont; 15.11.2017