При реализации обмена сообщениями между агентами (или активными объектами) нужно иметь очереди заявок. В некоторых случаях в очередь помещают объект-сообщение и извлекшая его сторона должна выяснить, что же это за объект и как с ним следует поступить. В других же случаях заявкой должен быть объект, который нужно запустить на выполнение, а он сам уже разберется, что и как ему делать.
Если нужно, что бы заявкой в очереди сообщений был какой-то самостоятельный объект, то в традиционном объектно-ориентированном подходе нужно было бы определить базовый класс, для примера, demand_t с виртуальным методом handle(), от которого нужно было бы наплодить столько наследников, сколько вариантов поведения требовала бы прикладная логика. И, в старом-добром C++, все это делалось бы на объектах, да еще и с применением какого-то варианта умного указателя, т.к. в очереди заявок сохранялись бы указатели на demand_t (а в самом хардкорном варианте в очереди были бы голые указатели, за временем жизни которых нужно было бы следить вручную).
Но стандарт C++11 уже несколько лет как утвержден. И теперь у C++ разработчиков есть выбор: делать ли такие заявки посредством объектных иерархий или же задействовать std::function (а так же лямбда-функции с замыканиями). Но, если делать выбор в пользу std::function, то не придется ли платить за это больше, чем за объекты и std::shared_ptr?
Ради проверки этого я написал для себя небольшой тест, исходный текст которого упрятан под кат. Проверял под VC++11, VC++12 и GCC 4.8.2 под 64-х битовым Windows 7.
Upd. Ув.тов.Андрей Валяев обнаружил ошибку в моем коде. После ее исправления выяснилось, что код на std::function где-то на 35-40% медленнее кода с std::shared_ptr и объектами.
Небольшой дисклаймер: в приведенном ниже коде можно было oo_demand_type_N сделать компактнее. Просто так больше похоже на то, что будет в реальной жизни, когда каждый наследник demand_t будет иметь кучу собственного уникального кода.
И еще одно небольшое дополнение напоследок: думаю, ситуация, когда заявка будет иметь всего один метод handle, т.е. когда объект-заявка может быть представлена функциональным объектом один-в-один, является частным случаем. На мой взгляд, велика вероятность, что у объекта-заявки будет несколько виртуальных методов. Если не сразу, то в процессе эволюции кода. И если иерархия конкретных заявок изначально представлена в виде объектной иерархии, то произвести модификацию кода с объектами будет проще, чем с std::function.
Суть теста: есть разные типы заявок, которые делают разные действия. Есть очередь заявок, есть генератор заявок. Для простоты сделано так, что заявок всего три типа и каждый тип заявки обращается к генератору с тем, чтобы генератор сделал заявку следующего типа. Т.е. заявка типа 1 приводит к генерации заявки типа 2, та к генерации заявки типа 3, а та -- к генерации заявки типа 1. И так по кругу, пока не будет проведено достаточное количество итераций.
При этом тест устроен так, что в очереди всегда находится только одна заявка. Т.ч. накладные расходы на саму очередь минимальны и одинаковы для обоих вариантов.
#include <iostream> #include <queue> #include <memory> #include <functional> #include <ctime> const size_t ITERATIONS = 100000000; // // Functional demands // typedef std::function< void(void) > fn_demand_t; typedef std::queue< fn_demand_t > fn_demand_queue_t; class fn_demand_generator_t { public : void on_type_1( fn_demand_queue_t & queue ) { queue.push( [this, &queue] { this->on_type_2( queue ); } ); } void on_type_2( fn_demand_queue_t & queue ) { queue.push( [this, &queue] { this->on_type_3( queue ); } ); } void on_type_3( fn_demand_queue_t & queue ) { queue.push( [this, &queue] { this->on_type_1( queue ); } ); } }; void fn_test() { fn_demand_queue_t queue; fn_demand_generator_t generator; generator.on_type_1( queue ); for( size_t i = 0; i != ITERATIONS; ++i ) { auto d = queue.front(); queue.pop(); d(); } } // // Object-oriented demands. // class oo_demand_t { public : virtual ~oo_demand_t() {} virtual void handle() = 0; }; typedef std::shared_ptr< oo_demand_t > oo_demand_shptr_t; typedef std::queue< oo_demand_shptr_t > oo_demand_queue_t; class oo_demand_generator_t { public : void on_type_1( oo_demand_queue_t & queue ); void on_type_2( oo_demand_queue_t & queue ); void on_type_3( oo_demand_queue_t & queue ); }; class oo_demand_type_1 : public oo_demand_t { private : oo_demand_generator_t & m_generator; oo_demand_queue_t & m_queue; public : oo_demand_type_1( oo_demand_generator_t & generator, oo_demand_queue_t & queue ) : m_generator( generator ) , m_queue( queue ) {} virtual void handle() { m_generator.on_type_1( m_queue ); } }; class oo_demand_type_2 : public oo_demand_t { private : oo_demand_generator_t & m_generator; oo_demand_queue_t & m_queue; public : oo_demand_type_2( oo_demand_generator_t & generator, oo_demand_queue_t & queue ) : m_generator( generator ) , m_queue( queue ) {} virtual void handle() { m_generator.on_type_2( m_queue ); } }; class oo_demand_type_3 : public oo_demand_t { private : oo_demand_generator_t & m_generator; oo_demand_queue_t & m_queue; public : oo_demand_type_3( oo_demand_generator_t & generator, oo_demand_queue_t & queue ) : m_generator( generator ) , m_queue( queue ) {} virtual void handle() { m_generator.on_type_3( m_queue ); } }; inline void oo_demand_generator_t::on_type_1( oo_demand_queue_t & queue ) { queue.push( std::make_shared< oo_demand_type_2 >( *this, queue ) ); } inline void oo_demand_generator_t::on_type_2( oo_demand_queue_t & queue ) { queue.push( std::make_shared< oo_demand_type_3 >( *this, queue ) ); } inline void oo_demand_generator_t::on_type_3( oo_demand_queue_t & queue ) { queue.push( std::make_shared< oo_demand_type_1 >( *this, queue ) ); } void oo_test() { oo_demand_queue_t queue; oo_demand_generator_t generator; generator.on_type_1( queue ); for( size_t i = 0; i != ITERATIONS; ++i ) { auto d = queue.front(); queue.pop(); d->handle(); } } // // Benchmarking // template< class TEST_CASE > void run_and_measure( const char * test_name, TEST_CASE test_case ) { const std::clock_t begin = std::clock(); test_case(); const std::clock_t end = std::clock(); const double duration = std::difftime(end,begin) / CLOCKS_PER_SEC; std::cout << test_name << ": " << duration << "s" << std::endl; } int main() { run_and_measure( "fn_demands", fn_test ); run_and_measure( "oo_demands", oo_test ); } |
Еще на тему использования функциональных объектов в C++11: std::function и лямбды дают возможность к применению ФПшных приемов.
Комментариев нет:
Отправить комментарий