Как я могу избежать dynamic_cast в моем коде на С++?
Скажем, у меня есть следующая структура классов:
class Car;
class FooCar : public Car;
class BarCar : public Car;
class Engine;
class FooEngine : public Engine;
class BarEngine : public Engine;
Пусть также дать Car
дескриптор его Engine
. A FooCar
будет создан с помощью FooEngine*
, а BarCar
будет создан с помощью BarEngine*
. Есть ли способ организовать вещи, поэтому объект FooCar
может вызывать функции-члены FooEngine
без downcasting?
Вот почему структура класса выложена так, как сейчас:
- Все
Car
имеют Engine
. Кроме того, a FooCar
будет использовать только FooEngine
.
- Есть данные и алгоритмы, разделяемые всеми
Engine
, которые я бы предпочел не копировать и вставлять.
- Возможно, мне захочется написать функцию, которая требует, чтобы
Engine
знал о ее Car
.
Как только я набрал dynamic_cast
при написании этого кода, я знал, что, вероятно, что-то не так. Есть ли лучший способ сделать это?
UPDATE:
Основываясь на ответах, данных до сих пор, я склоняюсь к двум возможностям:
- Имейте
Car
для предоставления чистой виртуальной функции getEngine()
. Это позволило бы FooCar
и BarCar
иметь реализации, возвращающие правильный тип Engine
.
- Поглотите все функциональные возможности
Engine
в дереве наследования Car
. Engine
был разбит по причинам технического обслуживания (чтобы сохранить материал Engine
в отдельном месте). Это компромисс между наличием меньших классов (малым по строкам кода) и меньшим количеством больших классов.
Есть ли сильное предпочтение сообщества для одного из этих решений? Есть ли третий вариант, который я не рассматривал?
Ответы
Ответ 1
Я предполагаю, что автомобиль содержит указатель на двигатель, и поэтому вы оказываетесь удручающим.
Выньте указатель из базового класса и замените его на чистую виртуальную функцию get_engine(). Затем ваши FooCar и BarCar могут содержать указатели на правильный тип двигателя.
(Edit)
Почему это работает:
Поскольку виртуальная функция Car::get_engine()
вернет ссылку или указатель, С++ позволит производным классам реализовать эту функцию с другим типом возврата, если тип возврата отличается только тем, что является более производным.
Это называется ковариантными типами возврата и позволит каждому типу Car
возвращать правильный Engine
.
Ответ 2
Единственное, что я хотел добавить: этот дизайн уже плохо пахнет мне из-за того, что я называю параллельными деревьями.
В принципе, если вы закончите параллельные иерархии классов (как у вас с автомобилем и двигателем), вы просто просите о проблемах.
Я бы передумал, если у Engine (и даже Car) должны быть подклассы, или все это просто разные экземпляры тех же соответствующих базовых классов.
Ответ 3
Можно также планировать тип двигателя следующим образом
template<class EngineType>
class Car
{
protected:
EngineType* getEngine() {return pEngine;}
private:
EngineType* pEngine;
};
class FooCar : public Car<FooEngine>
class BarCar : public Car<BarEngine>
Ответ 4
Я не понимаю, почему автомобиль не может состоять из двигателя (если BarCar всегда будет содержать BarEngine). Двигатель имеет довольно прочные отношения с автомобилем.
Я бы предпочел:
class BarCar:public Car
{
//.....
private:
BarEngine engine;
}
Ответ 5
Может ли FooCar использовать BarEngine?
Если нет, вы можете использовать AbstractFactory для создания правильного объекта автомобиля с правильным движком.
Ответ 6
Вы можете сохранить FooEngine в FooCar, BarEngine в BarCar
class Car {
public:
...
virtual Engine* getEngine() = 0;
// maybe add const-variant
};
class FooCar : public Car
{
FooEngine* engine;
public:
FooCar(FooEngine* e) : engine(e) {}
FooEngine* getEngine() { return engine; }
};
// BarCar similarly
Проблема с этим подходом заключается в том, что получение движка - это виртуальный вызов (если вас это беспокоит), а метод установки движка в Car
потребует понижения.
Ответ 7
Я думаю, что это зависит, если Engine
используется только частным образом Car
и его дочерними элементами или если вы также хотите использовать его в других объектах.
Если функции Engine
не относятся к Car
s, я бы использовал метод virtual Engine* getEngine()
вместо сохранения указателя в базовом классе.
Если его логика специфична для Car
s, я бы предпочел поместить общие данные/логику Engine
в отдельный объект (не обязательно полиморфный) и сохранить реализацию FooEngine
и BarEngine
в их соответствующих Car
дочерний класс.
При повторном использовании реализации больше, чем наследование интерфейсов, состав объектов часто обеспечивает большую гибкость.
Ответ 8
Microsoft COM довольно неуклюж, но у него есть новая концепция - если у вас есть указатель на интерфейс объекта, вы можете запросить его, чтобы узнать, поддерживает ли он какие-либо другие интерфейсы, используя QueryInterface. Идея состоит в том, чтобы разбить класс Engine на несколько интерфейсов, чтобы каждый из них можно было использовать независимо.
Ответ 9
Если я не пропустил что-то, это должно быть довольно тривиально.
Лучший способ сделать это - создать чистые виртуальные функции в Engine, которые затем требуются в производных классах, которые должны быть созданы.
Дополнительным кредитным решением, вероятно, будет иметь интерфейс IEngine, который вы переходите к своему автомобилю, и каждая функция в IEngine является чистой виртуальной. У вас может быть "BaseEngine", который реализует некоторые функции, которые вы хотите (т.е. "Совместно" ), а затем у вас есть листы.
Интерфейс хорош, если у вас есть что-то, что вы хотите "выглядеть" как движок, но, вероятно, нет (т.е. макетов тестовых классов и т.д.).
Ответ 10
Есть ли способ организовать вещи, поэтому объект FooCar может вызывать функции-члены FooEngine без downcasting?
Вот так:
class Car
{
Engine* m_engine;
protected:
Car(Engine* engine)
: m_engine(engine)
{}
};
class FooCar : public Car
{
FooEngine* m_fooEngine;
public:
FooCar(FooEngine* fooEngine)
: base(fooEngine)
, m_fooEngine(fooEngine)
{}
};