Как различать объекты в полиморфном списке?
Пусть есть какой-нибудь интерфейс, например геометрической фигуры, и у него есть 3 наследника: квадрат, треугольник, 5-угольник. Пусть указатели на них хранятся в одном векторе указателей. Возникает задача, как раскрасить все квадраты в красный цвет, если мы не можем отличить тип фигуры через полиморфный указатель. Я знаю 2 решеня:
- Динамик-каст, что является каким-то костылём
- Добавить поле вид внутрь объекта, что тоже сомнительно.
Как тогда покрасить в красный цвет только квадраты? Как с такой проблемой работать в общем случае?
Ответы (3 шт):
Никак, полиморфный список он не для этого :-)
В общем случае, тут может сработать принцип Tell, Dont Ask. Когда мы не спрашиваем у каждого объекта какой он и не говорим ему по результатам его ответа что ему делать, а сразу помещаем эту логику в объект, чтобы он сам решал, удовлетворяет ли он условиям и имеет ли нужные возможности.
Таким образом у нас может быть более хорошая инкапсуляция логики и полиморфизм, когда внешней системе даже не важно знать что такое квадрат, а достаточно просто "геометрической фигуры" (что по сути и декларируется вашим вектором указателей).
- Динамик-каст, что является каким-то костылём
- Добавить поле вид внутрь объекта, что тоже сомнительно.
Второе - ничем не лучше чем первое (возможно второе быстрее; если вам это важно - профилируйте). В первом нет ничего плохого, иногда это лучшее решение.
Есть три решения, каждое для своих случаев:
- Виртуальная функция.
dynamic_cast
(или сравнениеtypeid
, если нужен конкретный класс а не его потомки)
(или поле с указанием типа класса, если это в вашем случае чем-то лучше)- Рядом с объектом хранить указатель на функцию, которая делает то что нужно.
1 - Вариант по умолчанию.
2,3 - Если функции не место в родителе. Например, если классы используются во всем приложении, а такая раскраска нужна в одном месте. Тогда странно тащить ее в общего родителя. (Либо, если родитель - библиотечный, и его нельзя менять.)
3 удобнее, если есть где хранить эту функцию (особенно если объект создаете вы сами - тогда можно и правильную функцию подложить сразу, не изучая тип dynamic_cast
ами), а 2 - если объекты хранятся где-то снаружи, и никакие метаданные к ним не подложить.
Бывают ярые противники dynamic_cast
ов (см. соседний ответ, не в обиду @Kromster), видимо потому что их ругают в умных книжках. Но я пока ни от кого не получил нормального объяснения, что делать в ситуации выше, когда нельзя менять родителя, или функционал слишком специфичный чтобы его туда тащить.
Определяете базовый класс для "заполнителей":
struct Filler
{
virtual ~Filler() = default;
virtual void operator()(Square&) const {}
virtual void operator()(Triangle&) const {}
// ...
};
В базовый класс для фигур добавляете чисто виртуальную функцию fill
:
struct Shape
{
virtual ~Shape() = default;
// ...
virtual void fill(const Filler&) = 0;
};
В наследниках Shape переопределяете эту функцию:
struct Square : Shape
{
void fill(const Filler& filler)
{
filler(*this);
}
};
Определяете "заполнитель" только для квадратов:
struct SquareRedFiller
{
void operator()(Square& s){ /* code */ }
};
Кажется, все :) В SquareRedFiller
мы переопределили только оператор для квадратов, все остальные - реализация по умолчанию (которая ничего не делает).