Сохранение типа в базовом классе считается плохим программированием
Я хочу знать тип моего класса при компиляции, и я хочу знать, считается ли моя идея плохим программированием или действительно ли она жизнеспособна. Может поправить меня, если есть лучший способ понять это.
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 );
}
}