C++: горизонтальный полиморфизм с dynamic_cast
Введение⌗
Предположим, что мы разрабатываем приложение для военной симуляции, в котором имеется поддержка разнообразных по своим возможностям и характеристикам боевых платформ. В дизайне нашей системы мы решили отказаться от громоздкой иерархии наследования в пользу набора независимых друг от друга чистых виртуальных классов (интерфейсов), подмножество которых реализуется конкрециями через множественное наследование C++ (которое очень даже безопасно и применимо в случаях с несвязанными интерфейсами).
C++ предоставляет оператор dynamic_cast<T>()
для безопасного перехода (т.е.
преобразования указателя) между интерфейсами, имплементированными данной
конкрецией. При попытке кастинга указателя к интерфейсу, компилятор проверяет,
действительно ли у данного имплементора есть поддержка спецификации этого
интерфейса и возвращает корректный указатель на заимплеменченный интерфейс или
nullptr
, если конкреция не поддерживает указанную спецификацию.
Дизайн⌗
В примере ниже наш код состоит из:
- нескольких интерфейсов: боевая платформа в принципе
IPlatform
, поддержка позиционирования (координат)IPositionCapablePlatform
, поддержка возможности быть пассажиром (спешившейся пехотой)IDismountCapablePlatform
, поддержка возможности быть бронетранспортёром (закружать пассажирова на борт)ICarrierCapablePlatform
- абстрактного класса, для удобства штампования конкретных платформ (так как все
наши платформы суть позиционируемые платформы)
BasePlatform
- конкретных платформ, унаследованных от базового класса:
TankPlatform
,InfantryPlatform
,APCPlatform
- класса
Unit
единицы (юнита), который содержит номенклатурный номер и собственно саму платформу (наверняка должна поддерживаться толькоIPlatform
) - кода создания (инстанциации) юнитов с различными платформами
- кода полиморфической логики, динамически исследующей юниты на поддержку
платформой различных спецификаций: тут-то мы и задействуем
dynamic_cast<T>()
, осуществляя “горизонтальный полиморфизм”
Вот схематичная UML-диаграмма классов.
Код⌗
Чистые виртуальные классы⌗
Определим чистые виртуальные классы. Никакого наследования и вертикального усложнения – по дизайну вся наша система - плоская.
enum class PlatformType {
infantry,
apc,
armor
};
/*
* Интерфейсы
*/
class IPlatform {
public:
virtual std::string getName() = 0;
virtual PlatformType getType() = 0;
};
class IPositionCapablePlatform {
public:
virtual std::pair<int, int> getPosition() = 0;
virtual void setPosition(int, int) = 0;
};
class IDismountCapablePlatform {
public:
virtual unsigned getDismountSize() = 0;
};
class ICarrierCapablePlatform {
public:
virtual unsigned getCarrierCapacity() = 0;
virtual bool load(IDismountCapablePlatform*) = 0;
virtual void unload(IDismountCapablePlatform*) = 0;
virtual std::vector<IDismountCapablePlatform*> listCarrier() = 0;
};
Базовый класс⌗
Для удобства (чтобы каждый раз не имплементить IPlatform
и
IPositionCapablePlatform
в большинстве платформ симуляции) создаём абстрактный
базовый класс, от которого потом уже будем наследоваться в конкретных
платформах. Класс - абстрактный (т.е. неинстанциируемый), поскольку отсутствует
реализация getType()
интерфейса IPlatform
.
class BasePlatform :
public IPlatform,
public IPositionCapablePlatform {
private:
std::string name_;
std::pair<int, int> position_;
public:
/* ctor */
BasePlatform(std::string nam, std::pair<int, int> pos)
: name_(nam), position_(pos) {}
// IPositionCapablePlatform interface
protected:
std::pair<int, int> getPosition() override
{
return position_;
}
void setPosition(int x, int y) override
{
position_ = std::make_pair(x, y);
}
// IPlatform interface
protected:
std::string getName() override
{
return name_;
}
};
Конкретные классы⌗
Тривиальные платформы для воображаемой пехоты, танков и бронетранспортёров (APC, Armored Personnel Carrier):
/*
* Пехота
*/
class InfantryPlatform :
public BasePlatform,
public IDismountCapablePlatform {
private:
unsigned size_;
public:
InfantryPlatform(std::string nam, std::pair<int, int> pos, unsigned siz)
: BasePlatform(nam, pos), size_(siz) { }
// IDismountCapablePlatform interface
private:
unsigned getDismountSize() override
{
return size_;
}
// IPlatform interface
private:
PlatformType getType() override
{
return PlatformType::infantry;
}
};
/*
* Танки
*/
class TankPlatform : public BasePlatform {
public:
TankPlatform(std::string nam, std::pair<int, int> pos)
: BasePlatform(nam, pos) { }
// IPlatform interface
private:
PlatformType getType() override
{
return PlatformType::armor;
}
};
/*
* Бронетранспортёры (меняем capacity в завизимости от загрузки кузова)
*/
class APCPlatform :
public BasePlatform,
public ICarrierCapablePlatform {
private:
unsigned capacity_;
std::vector<IDismountCapablePlatform*> dismounts_;
public:
/* ctor */
APCPlatform(std::string nam, std::pair<int, int> pos, unsigned cap)
: BasePlatform(nam, pos), capacity_(cap) {}
// IPlatform interface
private:
PlatformType getType() override
{
return PlatformType::apc;
}
// ICarrierCapablePlatform interface
private:
unsigned getCarrierCapacity() override
{
return capacity_;
}
bool load(IDismountCapablePlatform* dp) override
{
if (capacity_ >= dp->getDismountSize()) {
dismounts_.push_back(dp);
capacity_ -= dp->getDismountSize();
return true;
} else
return false;
}
void unload(IDismountCapablePlatform* dp) override
{
for (auto it = dismounts_.begin(); it != dismounts_.end(); ++it)
if (dp == *it) {
capacity_ += dp->getDismountSize();
dismounts_.erase(it);
}
}
std::vector<IDismountCapablePlatform*> listCarrier() override
{
return dismounts_;
}
};
Объемлющий класс юнитов⌗
Для наглядности “вложим” нашу концепцию платформы в некий объемлющий класс. Назовём
его Unit
. Юнит содержит указатель лишь на самую “примитивную по
функциональности” платформу IPlatform
– все объекты нашей симуляции так или
иначе реализуют этот минимальный интерфейс.
class Unit {
private:
unsigned id_;
IPlatform* platform_;
public:
Unit(unsigned id, IPlatform* pl) : id_(id), platform_(pl) {
}
unsigned getId() {
return id_;
}
IPlatform* getPlatform() {
return platform_;
}
};
Вспомогательная функция проверки поддержки интерфейса⌗
С помощью dynamic_cast<T>()
мы используем горизонтальный полиморфизм –
запрашиваем преобразование указателя нашего минимального IPlatform
на
некоторый другой (возможно, поддерживаемый) интерфейс. При неудачном кастинге
компилятор возвращает nullptr
, что мы и отлавливаем.
/* helper */
template<class P>
bool isPlatform(IPlatform* basep) {
P* tryp = dynamic_cast<P*>(basep);
return (tryp != nullptr);
}
Тест⌗
Инстанциация⌗
Создаём юниты на базе различных конкретных платформ с проивзольными параметрами:
std::vector<Unit*> uv;
uv.push_back(new Unit{0, new TankPlatform{"Tank T-90U", std::make_pair(10, 20)}} );
uv.push_back(new Unit{1, new InfantryPlatform{"AT Infantry", std::make_pair(50, 60), 4}});
uv.push_back(new Unit{2, new InfantryPlatform{"Infantry Squad", std::make_pair(5, 6), 9}});
uv.push_back(new Unit{3, new APCPlatform{"BMP2", std::make_pair(10, 20), 12}} );
uv.push_back(new Unit{4, new APCPlatform{"BMP3", std::make_pair(70, 80), 9}} );
uv.push_back(new Unit{5, new TankPlatform{"Tank T-72B", std::make_pair(30, 40)}} );
uv.push_back(new Unit{6, new TankPlatform{"Tank T-72B", std::make_pair(50, 60)}} );
Полиморфическая работа с юнитами⌗
Вот как мы можем работать с юнитами, проверяя их платформы на наличие нужной функциональности (и задействуя её).
void logic(std::vector<Unit*> uv) {
for (Unit* u : uv) {
IPlatform* p = u->getPlatform();
printf("Unit %u, name: %s:\n", u->getId(), p->getName().c_str());
if (isPlatform<IPositionCapablePlatform>(p)) {
auto pos = dynamic_cast<IPositionCapablePlatform*>(p)->getPosition();
printf("\tPosition: %d, %d\n", pos.first, pos.second);
}
if (isPlatform<IDismountCapablePlatform>(p)) {
IDismountCapablePlatform* dcp = dynamic_cast<IDismountCapablePlatform*>(p);
unsigned size = dcp->getDismountSize();
printf("\tDismount size: %u\n", size);
}
if (isPlatform<ICarrierCapablePlatform>(p)) {
ICarrierCapablePlatform* ccp = dynamic_cast<ICarrierCapablePlatform*>(p);
auto cap = ccp->getCarrierCapacity();
printf("\tCarrier capacity: %u\n", cap);
}
}
}
Результат:
Unit 0, name: Tank T-90U:
Position: 10, 20
Unit 1, name: AT Infantry:
Position: 50, 60
Dismount size: 4
Unit 2, name: Infantry Squad:
Position: 5, 6
Dismount size: 9
Unit 3, name: BMP2:
Position: 10, 20
Carrier capacity: 12
Unit 4, name: BMP3:
Position: 70, 80
Carrier capacity: 9
Unit 5, name: Tank T-72B:
Position: 30, 40
Unit 6, name: Tank T-72B:
Position: 50, 60
А так мы можем грубо описать логику автоматической посадки всей воображаемой пехоты (всех юнитов у которых “платформа” поддерживает функции пехотинцев) во все доступные бронетранспортёры (все юниты, которые поддерживают загрузку в себя пехоты). Если количество человек в указанном юните превышает количество свободных в БТР мест, то логика пытается рассадить их в следующий незанятый бронетранспортёр.
std::vector<IDismountCapablePlatform*> dcpv;
std::vector<ICarrierCapablePlatform*> ccpv;
/*
* Посадка
*/
for (IDismountCapablePlatform* dcp : dcpv) {
for (ICarrierCapablePlatform* ccp : ccpv) {
if(ccp->load(dcp))
break;
}
}
/*
* Вывод на экран содержимого БТР-ов
*/
for (ICarrierCapablePlatform* ccp : ccpv) {
IPlatform* p = dynamic_cast<IPlatform*>(ccp);
printf("Platform name: %s", p->getName().c_str());
printf("\tCargo free: %u\n", ccp->getCarrierCapacity());
for (IDismountCapablePlatform* dcp : ccp->listCarrier()) {
printf("\tDismount of size %u inside\n", dcp->getDismountSize());
}
}
Результат:
Platform name: BMP2 Cargo free: 8
Dismount of size 4 inside
Platform name: BMP3 Cargo free: 0
Dismount of size 9 inside
Заметим, что на входе logic()
мы получили вектор юнитов без единого намёка на
наличие или отсутствие в нём платформ, допускающих посадку или спешивание.