Введение

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

C++ предоставляет оператор dynamic_cast<T>() для безопасного перехода (т.е. преобразования указателя) между интерфейсами, имплементированными данной конкрецией. При попытке кастинга указателя к интерфейсу, компилятор проверяет, действительно ли у данного имплементора есть поддержка спецификации этого интерфейса и возвращает корректный указатель на заимплеменченный интерфейс или nullptr, если конкреция не поддерживает указанную спецификацию.

Дизайн

В примере ниже наш код состоит из:

  • нескольких интерфейсов: боевая платформа в принципе IPlatform, поддержка позиционирования (координат) IPositionCapablePlatform, поддержка возможности быть пассажиром (спешившейся пехотой) IDismountCapablePlatform, поддержка возможности быть бронетранспортёром (закружать пассажирова на борт) ICarrierCapablePlatform
  • абстрактного класса, для удобства штампования конкретных платформ (так как все наши платформы суть позиционируемые платформы) BasePlatform
  • конкретных платформ, унаследованных от базового класса: TankPlatform, InfantryPlatform, APCPlatform
  • класса Unit единицы (юнита), который содержит номенклатурный номер и собственно саму платформу (наверняка должна поддерживаться только IPlatform)
  • кода создания (инстанциации) юнитов с различными платформами
  • кода полиморфической логики, динамически исследующей юниты на поддержку платформой различных спецификаций: тут-то мы и задействуем dynamic_cast<T>(), осуществляя “горизонтальный полиморфизм”

Вот схематичная UML-диаграмма классов.

Class diagram

Код

Чистые виртуальные классы

Определим чистые виртуальные классы. Никакого наследования и вертикального усложнения – по дизайну вся наша система - плоская.

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() мы получили вектор юнитов без единого намёка на наличие или отсутствие в нём платформ, допускающих посадку или спешивание.