Сохранение типа в базовом классе считается плохим программированием

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

class Base {
   int type = 0;
}

class Derivative : public Base{
   Derivative(){
      type = 1;
      SomeObject1 o;
      SomeAnotherObject o1;

   }
}

class Derivative2 : public Base{
   Derivative2(){
      type = 2;
      RandomObject test;
      AnotherObject v;
   }
}

Какой-то метод, который получает myBaseClass как Base:

if(myBaseClass.type == 1){
   Derivative d = static_cast<Derivative>(myBaseClass);
   d.o;
   d.o1;
}

if(myBaseClass.type == 2){
   Derivative2 d = static_cast<Derivative2>(myBaseClass);
   d.test;
   d.v;
}

На мой взгляд, было бы необычно писать виртуальные методы для всех различных объектов

3 ответа

Сохранение типа в базовом классе считается плохим программированием

Определенно да!

При использовании полиморфного виртуального дизайна вам не нужно хранить эту дополнительную информацию в базовом классе. Компилятор уже делает это для вас:

class Base {
protected:
   virtual ~Base() {} // <<<<<<<<<<<<<
}; // Note the ;!

class Derivative : public Base{
};

class Derivative2 : public Base{
};

Вы всегда можете обнаружить реальный тип класса из Base указатель или ссылка с dynamic_cast затем:

Base* pd1 = new Derivative();
Base* pd2 = new Derivative2();

if(dynamic_cast<Derivative>(pd1)) { // Yields true
}
if(dynamic_cast<Derivative>(pd2)) { // Yields false
}

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

Скорее, вы должны ввести некоторые интерфейсы в виде определений чисто виртуальных функций:

class Base {
protected:
   virtual ~Base() {}
public:
   virtual void DoSomething() = 0;
};

class Derivative : public Base{
public:
   void DoSomething() override {
       // provide an implementation specific for Derivative 
   }
};

class Derivative2 : public Base{
public:
   void DoSomething() override {
       // provide an implementation specific for Derivative2 
   }
};

Это позволяет вам звонить DoSomething() не зная конкретного типа, который реализует эту функцию:

Base* pd1 = new Derivative();
Base* pd2 = new Derivative2();

pd1->DoSomething(); // calls Derivative specific implementation
pd2->DoSomething(); // calls Derivative2 specific implementation

Безопасное и эффективное использование static_cast используйте вместо этого CRTP:

template<typename Derived>
class Base {
public:
    void DoSomething() {
         static_cast<Derived*>(this)->DoSomething();
    }
};

class Derivative : public Base<Derivative> {
};

class Derivative2 : public Base<Derivative2> {
};

Есть способы использования этой техники, которые, по крайней мере, правдоподобны. Одна из тех, что я видел, включала иерархию классов, чьи экземпляры нужно было настраивать пользователем (из Python), а затем использовать в критичном для производительности коде (в C++). Базовый класс обеспечил getType() метод, который возвратил перечисление; код оболочки в Python вызывал это, чтобы узнать, какой интерфейс предложить пользователю. Межъязыковой код часто заставляет использовать простые методы, подобные этой, основанные на согласованных целочисленных метках.

В целом, иногда хорошие принципы проектирования, такие как MVC, поощряют такой вид организации. Даже если разные слои написаны на одном языке, для базовых объектов модели не обязательно иметь такие методы, как makeQtWidgets(), так как он требует, чтобы этот уровень знал не только о библиотеке GUI, но также о макете и потоке управления пользовательского интерфейса.

Практический момент: чтобы избежать ситуации, когда производный класс не может указать свой тип, базовый класс должен требовать значение в своем конструкторе:

struct Base {
  enum Type { derived1, derived2 };
  Base(Type t) : typ(t) { /* ... */ }
  virtual ~Base()=0;

  Type getType() const {return typ;}
  // ...

private:
  Type typ;
};

struct Derived1 : Base {
  Derived1() : Base(derived1) { /* ... */ }
  // ...
};

Вы могли бы также поставить enum всех возможностей в базовом классе, так как уже должен быть центральный реестр значения для каждого производного класса, даже если это просто на бумаге. Это недостаток по сравнению с несколькими, упомянутыми другими: этот дизайн требует централизованного управления всеми классами, без возможности независимого расширения.

Наконец, несмотря на эту негибкость, клиенты должны всегда сталкиваться с уродливой возможностью объекта неожиданного типа:

void foo(const Base &b) {
  switch(b.getType()) {
  case Base::derived1: /* ... */ break;
  case Base::derived2: /* ... */ break;
  default:
    // what goes here?
  }
}

Вот (уродливый) подход, который я использовал несколько лет назад, когда взламывал писателя PDF. Похоже, это решает точно ту же проблему, что и у вас.

pdfArray::pdfArray(const pdfArray &src)
{
    vecObjPtrIter iter;
    pdfObj *ptr;
    mArray = new vecObjPtr;

    for (iter=src.mArray->begin(); iter!=src.mArray->end(); iter++)
    {
        ptr = *iter;

        if (typeid(*ptr) == typeid(pdfString))
            addItem( (pdfString*)ptr );

        if (typeid(*ptr) == typeid(pdfInt))
            addItem( (pdfInt*)ptr );

        if (typeid(*ptr) == typeid(pdfFloat))
            addItem( (pdfFloat*)ptr );

        if (typeid(*ptr) == typeid(pdfArray))
            addItem( (pdfArray*)ptr );
    }
}
Другие вопросы по тегам