О составных объектах

Модераторы: Hawk, Romeo, Absurd, DeeJayC, WinMain

Павел Легков
Сообщения: 6
Зарегистрирован: 06 апр 2017, 10:41

11 апр 2017, 10:21

Джентльмены, всем доброго времени суток.
Не приходилось ли Вам сталкиваться со следующей задачей:
Нужно создавать составные объекты из простых объектов, относящихся к некоторой иерархии классов. Вроде напрашивается паттерн «компоновщик», но беда в том, что поведение составного объекта определяется типами входящих в него объектов (грубо говоря, если в составной объект входят объекты классов A, B и C, то составной объект должен вести себя подобно объекту класса C). Т.е. составной объект должен знать типы входящих в него объектов, и от этого все прелести полиморфизма улетучиваются (
Может кто-нибудь сталкивался с чем-либо подобным и смог обойтись без использования RTTI?
С уважением, Павел.
Аватара пользователя
Romeo
Сообщения: 3091
Зарегистрирован: 02 мар 2004, 17:25
Откуда: Крым, Севастополь
Контактная информация:

11 апр 2017, 12:04

A, B и C имеют общий базовый? Если нет, я что-то вообще не могу представить обобщённую реализацию composite класса. Наверное, нужно чуть больше информации. Например ключевые куски кода.
Entites should not be multiplied beyond necessity @ William Occam
---
Для выделения С++ кода используйте конструкцию [ code=cpp ] Код [ /code ] (без пробелов)
---
Сообщение "Спасибо" малоинформативно. Благодарность правильнее высказать, воспользовавшись кнопкой "Reputation" в виде звёздочки, расположенной в левом нижнем углу рамки сообщения.
Skwoogey
Сообщения: 63
Зарегистрирован: 11 янв 2016, 02:25

11 апр 2017, 20:25

Если, как сказал Ромео, есть базовый, то я бы создал в нем виртуальную функцию GetID(), а в наследуемых возвращал бы разные значения через нее.
Павел Легков
Сообщения: 6
Зарегистрирован: 06 апр 2017, 10:41

12 апр 2017, 15:50

Добрый день!
Да, классы A, B и С имеют общего родителя – класс Father
У этих классов есть виртуальный метод DoSomething().
Появилась необходимость создать составной класс D. Думал его создать, как наследника от Father, и добавить в D член vector<Father*> parts_ для хранения указателей на составные части D.
Но беда в том, что я не могу написать D :: DoSomethig() следующим простым образом:

D :: DoSomethig()
{
for (std::vector<Father>::iterator i = parts_.begin(); i != parts_.end(); ++i)
{
(*i)->DoSomething();
}
}
Увы, поведение D по условию задачи должно зависеть, от того, объекты каких именно типов в него входят. Например, если в состав составного объекта типа D входит хотя бы один объект типа C, то независимо от количества объектов типа A и B, входящих в рассматриваемый элемент D, поведение D должно быть следующим:

D :: DoSomethig()
{
(*iC)->DoSomething();
}
где iC – итератор parts_, указывающий на объект типа С.
Павел Легков
Сообщения: 6
Зарегистрирован: 06 апр 2017, 10:41

12 апр 2017, 17:41

Не ну это уже совсем белый флаг) Тогда уж лучше RTTI. Просто всегда, когда используешь RTTI, не покидает ощущение, что изначально криво запроектировал систему.
Павел Легков
Сообщения: 6
Зарегистрирован: 06 апр 2017, 10:41

12 апр 2017, 17:41

Skwoogey писал(а):Если, как сказал Ромео, есть базовый, то я бы создал в нем виртуальную функцию GetID(), а в наследуемых возвращал бы разные значения через нее.

Не ну это уже совсем белый флаг) Тогда уж лучше RTTI. Просто всегда, когда используешь RTTI, не покидает ощущение, что изначально криво запроектировал систему.
Аватара пользователя
Romeo
Сообщения: 3091
Зарегистрирован: 02 мар 2004, 17:25
Откуда: Крым, Севастополь
Контактная информация:

12 апр 2017, 19:25

Совершенно верно, чем вводить GetID, уж лучше вызывать dynamic_cast, который, по факту, тоже использует таблицу виртуальных функций для скрытого вызова метода, возвращающего информацию о типе объекта. Этот механизм, по крайней мере стандартный, а использование стандартного механизма в любом случае лучше самописного решения.

Также соглашусь с тем, что когда начинаешь делать поочередный down cast и проверять, успешно ли всё прошло, то чаще всего это первый сигнал о том, что система спроектирована криво. Тем не менее, бывают случаи, когда так всё же сделать лучше, так как альтернатива куда более неочевидна, менее гибка, да и просто куда менее читабельна и воспринимаема.

Задам ещё один вопрос. А больше никаких хитростей с изменённым поведением потенциального композита нет? Просто если такая хитрость одна, то можно ввести скажем понятие "приоритета". Например, объявить в базовом классе метод:

Код: Выделить всё

virtual bool IsHighPriority() const = 0;
В наследниках А и B возвращать false, а в C вернуть true.

Тогда метод Container::DoSomething сначала провебежит по всем объектам и проверит, вернул ли кто-то true. Если вернул, то вызывает DoSomething у него и выходит. Если же никто не вернул, то вызывает DoSomething у всех.

Однако, если подобных "хитрых" отклонений от стандартного поведения композита будет много, то на каждый придётся придумывать отдельный метод, что крайне усложнит код. Тогда уж лучше dynamic_cast.
Entites should not be multiplied beyond necessity @ William Occam
---
Для выделения С++ кода используйте конструкцию [ code=cpp ] Код [ /code ] (без пробелов)
---
Сообщение "Спасибо" малоинформативно. Благодарность правильнее высказать, воспользовавшись кнопкой "Reputation" в виде звёздочки, расположенной в левом нижнем углу рамки сообщения.
Skwoogey
Сообщения: 63
Зарегистрирован: 11 янв 2016, 02:25

12 апр 2017, 23:22

так ваш IsHighPriotity по сути мой GetID, только я предполагал возвращение инта или чара, чтобы можно было точно определить класс.

Базовый:

Код: Выделить всё

virtual char GetId() = 0;
А:

Код: Выделить всё

char GetId()
{
	return 'A';
}
B:

Код: Выделить всё

char GetId()
{
	return 'B';
}
Чем мой метод хуже вашего? И как мне кажется (хотя я могу грубо ошибаться), оно быстрее и эффективнее, чем RTTI. Он ведь, насколько я понимаю, создает объект на основе переменной со своими полями и прочим. Опять же, я могу ошибаться, так как о RTTI у нас было лекций 1-2, а использовать не приходилось. Буду рад, если поправите.

Вообще я явно не в свою лигу лезу, поэтому прошу прощения, но вдруг. Хотя бы дискуссия получится интересная.
Аватара пользователя
Romeo
Сообщения: 3091
Зарегистрирован: 02 мар 2004, 17:25
Откуда: Крым, Севастополь
Контактная информация:

13 апр 2017, 12:40

RTTI по факту реализован через таблицу виртуальных функций. Большинство компиляторов действуют одинаково. Они создают специальный метод для каждого класса, где есть таблица виртуальных методов и помещают его в таблицу под индексом -1. Метод возвращает ссылку на статический объект типа type_info. Таким образом оператору dynamic_cast достаточно вызвать этот скрытый метод и сравнить является ли адрес возвращённого объекта таким же, как адрес указанного типа, к которому мы кастим. Это если на пальцах. На самом деле алгоритм несколько сложнее, так как он ещё умеет выявлять возможность кастинга с наследникам/предкам, с помощью дополнительных вызовов всё тех же неявных методов, но суть от этого не меняется. Отсюда, кстати, становится понятным почему по стандарту запрещён dynamic_cast, если у класса отсутствует таблица виртуальных методов.

Следующий вопрос, это чем лучше метод GetID или dynamic_cast с проверкой на nullptr. GetID однозначно будет работать несколько быстрее, так как не будет выполняться дополнительный код проверки наследников/предков, который зашит в dynamic_cast. Однако, в случае GetID, нам приходится руками создавать новый виртуальный метод, а так же руками его переопределять в каждом наследнике. В результате имеем лишний виртуальный метод и ненужные хлопоты с написанием дополнительного кода в каждом классе. Плата излишне велика. Я бы выбрал RTTI.

Ну и последнее. Зачем я заговорил о методе IsHighPriority. У него есть одно очень важное преимущество. Он уменьшает связность. Оба предыдущих варианта требовали от композитора знаний о ВСЕХ классах. Весь смысл композитора как раз в том, чтобы однородно работать со всеми содержащимися в нём объектами, не зная о том, какой же объект на самом деле скрыт под указателем на базу. Если мы вносим в композитор знания о все классах, то все плюсы полиморфизма просто перестают приносить нам плоды. Ведь если композитор знает о всех типах, то мы можем сделать внутри не один вектор для всех объектов, а много векторов на каждый тип. Затем сделать перегрузки для каждого метода Add, а также написать ручной обход всех векторов в каждом функциональном методе-делегате. После этого мы легко избавимся от виртуальных методов. Код, который получится в результате называют в программировании спагетти-кодом (подробнее на вики). Он громоздок, абсолютно нерасширяем, да и просто это "код с душком" (С) Scott Meyers.

И так, знание обо всех типах - это плохо. В то время как метод IsHighPriority позволяет не знать обо всех типах, а лишь разделить их да два множества - типы с высоким приоритетом и не с высоким. Да, мы платим цену лишнего виртуального метода, как и в случае GetID, но мы понимаем, за что платим. Мы уменьшаем связность кода.
Entites should not be multiplied beyond necessity @ William Occam
---
Для выделения С++ кода используйте конструкцию [ code=cpp ] Код [ /code ] (без пробелов)
---
Сообщение "Спасибо" малоинформативно. Благодарность правильнее высказать, воспользовавшись кнопкой "Reputation" в виде звёздочки, расположенной в левом нижнем углу рамки сообщения.
Павел Легков
Сообщения: 6
Зарегистрирован: 06 апр 2017, 10:41

27 апр 2017, 16:19

Если кому ещё интересен вопрос.
Сделал через RTTI, но потом понял, что протупил. Достаточно было сделать у составного элемента не один метод добавления дочернего элемента void Add(Father*), а столько методов Аdd, сколько есть классов в иерархии, т.е. void Add(A*), void Add(B*), void Add(C*)
Тогда через эти Addы я получаю информацию о типах дочерних элементов, и могу выбирать требуемую стратегию поведения составного элемента.
Ответить