dad>мне кажется адбстракция визитор не соответвует посталеной задаче..
Дык к предложенной постановке можно и ещё несколько вариантов придумать.
Визитор позволяет добавлять функциональность вообще не затрагивая иерархии. Посмотри внимательно как я ввёл дополнительный контейнер для dravable. Не пришлось ничего менять ни в базовых классах, ни в их наследниках.
Хотя, если иерархия часто перестраивается визитор — спорное решение.
dad>вот вариант с гранями не требующий переписывать провежуточные классы при добавлении новых функций (новые функци добавляются как внешние грани, а в базовом классе прописывается виртуаьный оператор получения грани)
Несколько комментариев:
dad>
dad>class drawable //грани для классов
dad>{
dad>public:
dad> virtual void doit() = 0;
//Не понятно зачем этот оператор. В дальнейшем коде он неиспользуется
dad> virtual operator bool () = 0;
dad>};
........
dad>template<class valueT> // обертка для класса имеющего грань
dad>class draw_face : public drawable
dad>{
dad> valueT* _p;
dad>public:
dad> draw_face(valueT* src) : _p(src) {}
//Здесь видимо надо проверить на 0, (код ниже подтверждает возможность 0)
dad> virtual void doit() { _p->draw() ; }
//Обычно равенство 0 трактуется как false а здесь наоборот
dad> virtual operator bool () { return _p == 0 ; }
dad>};
.............
dad>class engine
dad>{
dad> std::list<drawable*> _drawed;
dad> std::list<serialize*> _serlz;
dad> void add_draw(drawable* src)
dad> {
dad> if ( src ) _drawed.push_back(src);
//Здесь видимо подразумевался такой код:if ( src && *src ) _drawed.push_back(src);
dad> }
............
dad>public:
dad> void reg_obj(game_object& src)
dad> {
dad> add_serialize(src);
dad> add_draw(src);
dad> }
А вот чтобы реализовать unreg_obj придётся изрядно повозиться.
Для грани надо ввести метод, который отдаёт указатель на базовый объект.
После этого в цикле пройтись по контейнерам, кастируя елемент к грани, добывая указатель на базовый объект и сравнивая с запросившим.
dad> void draw_all()
dad> {
dad> for ( std::list<drawable*>::iterator it = _drawed.begin(); it != _drawed.end(); ++ it)
//Вот здесь происходит как минимум 2 виртуальных вызова
dad> (*it)->doit();
dad> }
............
dad>
Как локально оптимизировать в данном случае проход — мне с ходу в голову не приходит.
К тому-же есть некоторые проблемы с проходом по всем объектам — видимо придётся таки хранить список всех объектов.
Несомненное приимущество — простота изменений.
Вот краткое сравнение подходов — описываются дополнительные действия, требуюшиеся при поддержании подхода:
1) Добавлении класса в иерархию
Визитер — Приходится переписывать все визитеры (возможно существенное уменьшение при грамотном построении иерархии)
Грани — Просто дописываем класс, переопределяя функции получения нужных граней.
2) Довабление алгоритма:
Визитер — Пишется новый визитер.
Грани — Пишется базовая грань, пишется обёртка для грани, в базовый класс добавляется функция получения новой грани.
3) Память
Визитер — Объекты могут быть любого размера и распологаться где угодно (часто только VMT на стеке)
Грани — много дополнительных мелких объектов в куче
4) Быстордействие — одинаковое (2 виртуальных вызова). Хотя по моему "визитер" проще поддаётся оптимизации.
... << RSDN@Home 1.1.0 stable >>
Re[4]: "Хитрый" проход по контейнеру полиморфных элементов.
dad>>мне кажется адбстракция визитор не соответвует посталеной задаче.. T>Дык к предложенной постановке можно и ещё несколько вариантов придумать.
все варианты будут разновидностями одного и того же подхода.
Что касается визитора — на первый взгляд конструкция показалась мне несколько громоздкой,
и не отражающей предметную область. При повторном рассмотрении показалась вполне.
Правда, я так и не понял , если честно, — как функцинирует accept?
Замечания по моему коду — абсолютно справедливы /даже в школе всегда за небрежность понижали оценки/. Кроме извлечения — если заточить под хранение указателей — удалять тоже можно по указателю.
T>Вот краткое сравнение подходов — описываются дополнительные действия, требуюшиеся при поддержании подхода:
T>1) Добавлении класса в иерархию T>Визитер — Приходится переписывать все визитеры (возможно существенное уменьшение при грамотном построении иерархии) T>Грани — Просто дописываем класс, переопределяя функции получения нужных граней.
Грани еще требуют либо перекомпиляции всей иерархии либо замены граней, как таковых на
dynamic_cast , как предлагал форшбиттен, может еще можно множественное наследование (примеси) ?
T>2) Довабление алгоритма: T>Визитер — Пишется новый визитер. T>Грани — Пишется базовая грань, пишется обёртка для грани, в базовый класс добавляется функция получения новой грани.
T>3) Память T>Визитер — Объекты могут быть любого размера и распологаться где угодно (часто только VMT на стеке) T>Грани — много дополнительных мелких объектов в куче
T>4) Быстордействие — одинаковое (2 виртуальных вызова). Хотя по моему "визитер" проще поддаётся оптимизации.
Короче, поупражняться можно, но это пусть автор топика поупражняется и результаты выдаст, за что ему будет рахмат.
Веру-ю-у! В авиацию, в научную революци-ю-у, в механизацию сельского хозяйства, в космос и невесомость! Веру-ю-у! Ибо это объективно-о! (Шукшин)
Re: "Хитрый" проход по контейнеру полиморфных элементов.
Было принято следующее архитектурное решение:
1) на каждый поддерживаемый интерфейс — по очереди;
2) автоматическое формирование менеджеров, параметризованных базой, обеспечивающей интерфейс;
3) класс прослойка, предоставляющая сервисы для регистрации/разрегистрации объектов.
Надеюсь, следующий код пояснит мою мысль:
// slave.hpptemplate <class T>
class slave {
public:
slave() { manager<T>::instance()->reg(static_cast<T*>(this); }
virtual ~slave() { manager<T>::instance()->unreg(static_cast<T*>(this); }
};
// manager.hpptemplate <class T>
class manager {
public:
typedef std::set<T*> objects_queue;
static manager* instance() { return instance_ ? instance_ : instance_ = new manager(); }
void reg_object(T* go) { objects_.insert(go); }
void unreg_object(T* go){ objects_.erase(go); }
// действия над всеми объектамиtemplate <class func> void do_with_all(func what) {
std::for_each(objects_.begin(), objects_.end(), what);
}
// действия над одним объектомtemplate <class func> void do_with(T* go, func what) {
if (objects_.find(go) != objects_.end) what(go);
else throw std::logic_error("some problems here");
};
protected:
manager() { }
manager(const manager&) { }
objects_queue objects_;
private:
static manager* instance_;
};
///////////////////////////////////////////////////////////////////////////////
// Клепать классы, поддерживающие нужный интерфейс и автоматом регистрирующиеся
// у соответствующего менеджера теперь проще простого:
///////////////////////////////////////////////////////////////////////////////
// drawable.hpp
// Базовый класс для всех объектов, подлежащих отрисовкеclass drawable
: public slave<drawable> {
public:
virtual void draw() { render_system::instance()->draw(sprite_, coords_); }
protected:
sprite* sprite_;
rectangle<real> coords_;
};
// updateable.hpp
// Базовый класс для всех объектов, состояник которых нужно обновлять.class updateable
: public slave<updateable> {
public:
virtual void update() = 0;
};
///////////////////////////////////////////////////////////////////////////////
// Пример клиентского кода
///////////////////////////////////////////////////////////////////////////////
// waypoint.hppclass waypoint
: public updateable {
public:
virtual void update() { std::cout << "waypoint was updated" << std::endl; }
};
// soldier.hppclass soldier
: public updateable
, public drawable {
public:
virtual void update() { std::cout << "soldier was updated" << std::endl; }
virtual void draw() { std::cout << "soldier was drawn" << std::endl; }
}
// main.cppint main()
{
soldier* s1 = new soldier;
soldier* s2 = new soldier;
waypoint* wp = new waypoint;
manager<updateable>::instance()->do_with_all(std::mem_fun(updateable::update));
std::cout << std::endl;
manager<drawable>::instance()->do_with_all(std::mem_fun(drawable::draw));
}
Вывод:
soldier was updated
soldier was updated
waypoint was updated
soldier was drawn
soldier was drawn
Понятно, что в реальном коде мы воспользовались бы фабриками объектов, умными указателями и пр.
Преимущества:
1) Главное — простота производства клиентских классов.
2) Менеджер может выполнять над очередью объектов всяческие операции, а не только вызов функции с сигнатурой void T::function().
3) Большая часть ошибок отлавливается в compile-time.
Недостатки:
1) manager<updateable>::instance()->do_with_all(std::mem_fun(updateable::update)) все-таки смотрится жутковато для простого обновления объектов.
2) Первоначальная цель в наличии одной очереди не была достигнута. Мои изголятельства — всего лишь автоматическое формирование кода для нескольких очередей.
3) [ваши варианты]
Спасибо всем, кто принял участие в обсуждении.
Special thanks to Sergey "COMER" Shandar за идею и Ivan Kozlov за критику.
Re[2]: "Хитрый" проход по контейнеру полиморфных элементов.
dad wrote:
> Что касается визитора — на первый взгляд конструкция показалась мне несколько громоздкой, > и не отражающей предметную область. При повторном рассмотрении показалась вполне. > Правда, я так и не понял , если честно, — как функцинирует accept?
>> Что касается визитора — на первый взгляд конструкция показалась мне несколько громоздкой, >> и не отражающей предметную область. При повторном рассмотрении показалась вполне. >> Правда, я так и не понял , если честно, — как функцинирует accept?
ME>Ищи описание идиомы double dispatch.
Я в курсе двойной передачи, но она работает при гомоморфной иерархии,
я не пойму как она работает в нашем случае:
class any : public game_object
, public serializable
, public drawable
{ ->visit(v) { v->accept(*this); };
struct inserter_t : object_visiter {
void accept(game_object& obj) //{objects_.insert(obj);}void accept(serializable& obj)// {accept(static_cast<game_object&>(obj));}void accept(dravable& obj) // {
какой акцепт вызовется? /извините, но я туповат/
Веру-ю-у! В авиацию, в научную революци-ю-у, в механизацию сельского хозяйства, в космос и невесомость! Веру-ю-у! Ибо это объективно-о! (Шукшин)
Re[7]: "Хитрый" проход по контейнеру полиморфных элементов.
dad>Я в курсе двойной передачи, но она работает при гомоморфной иерархии,
точнее не при гомоморфной , а когда в базовый класс знает все о наследниках (используется для виртуальных операторов, напр),
либо класс акцептор, как в нашем случае, знает все об иерархии , или когда
каждый экземпляр знает с каким акцептором он работает, либо каждый экземпляр
должен переопределять visit метод для приведения себя к нужному типу ссылки
(по три раза вызывать, например, accept с нужным типом) т.е. он должен знать _КАК_ работает акцептор (визитор)
Веру-ю-у! В авиацию, в научную революци-ю-у, в механизацию сельского хозяйства, в космос и невесомость! Веру-ю-у! Ибо это объективно-о! (Шукшин)
Re[7]: "Хитрый" проход по контейнеру полиморфных элементов.
или несколько изменить реализацию visit для класса any
class any : public game_object, public serializable, public drawable {
public:
virtual void visit(object_visiter& visiter) {
visiter.accept(static_cast<game_object*>(*this));
visiter.accept(static_cast<serializable*>(*this));
visiter.accept(static_cast<drawable*>(*this));
}
};
Я бы предпочёл первый путь, но тут всё от задачи зависит. ;-в
... << RSDN@Home 1.1.0 stable >>
Re[4]: "Хитрый" проход по контейнеру полиморфных элементов.
T>Вот краткое сравнение подходов — описываются дополнительные действия, требуюшиеся при поддержании подхода:
T>1) Добавлении класса в иерархию T>Визитер — Приходится переписывать все визитеры (возможно существенное уменьшение при грамотном построении иерархии) T>Грани — Просто дописываем класс, переопределяя функции получения нужных граней. T>2) Довабление алгоритма: T>Визитер — Пишется новый визитер. T>Грани — Пишется базовая грань, пишется обёртка для грани, в базовый класс добавляется функция получения новой грани. T>3) Память T>Визитер — Объекты могут быть любого размера и распологаться где угодно (часто только VMT на стеке) T>Грани — много дополнительных мелких объектов в куче T>4) Быстордействие — одинаковое (2 виртуальных вызова). Хотя по моему "визитер" проще поддаётся оптимизации.
Забыл написать — Грани хороши тогда, когда надо отвязать клиентов от реализации. При этом подходе клиент может ничего не знать о классах реализации — ведб он работает только с интерфейсами. И реализации тоже ничего о клиенте знать ничего не надо.
Один из примеров маштабного использования технологии граней — COM.
... << RSDN@Home 1.1.0 stable >>
Re[8]: "Хитрый" проход по контейнеру полиморфных элементов.