Что такое function и зачем писать еще одну
Попробую дать краткую вольную формулировку: это объект, обладающий семантикой значения и позволяющий привести свободные функции и функции-члены классов к единому интерфейсу вызова. При необходимости, объект сохраняет контекст вызова, речь про this, что актуально для функций-членов классов.
Продвинутые реализации, вроде boost, предлагают такие интересные средства, как адаптирование функций с неподходящей сигнатурой под требуемый интерфейс вызова, а также возможность связывания аргументов с параметрами в момент инициализации. Но, на мой взгляд, подобные возможности лежат за рамками основной темы, заметно раздувают реализацию, но, при этом, далеко не всегда необходимы. Такие средства безусловно удобны и даже иногда полезны, но, по моему субъективному мнению, усложняют чтение и отладку кода. Пусть собственная реализация будет в чем-то проще, обладая при этом такими преимуществами, как, относительная лаконичность и простота.
Мотивация к продолжению поиска
Итак, постановка задачи: требуется класс, параметризуемый сигнатурой функции, определяющей интерфейс вызова, позволяющий связывать и позднее вызывать как свободные функции, так и функции-члены классов, сохраняя для последних контекст вызова. Возвращаясь к решению из предыдущей статьи, отвечает ли оно указанным требованиям, какие у него можно заметить недостатки, поддающиеся исправлению? Решение не сохраняет контекст вызова для функций-членов классов и, таким образом, теряет в универсальности применения. Внутри используются виртуальные функции, наследование и динамическое распределение памяти, что, как будет видно дальше, не является необходимым, можно сделать проще. Оговорюсь, что динамическое распределение памяти считаю основным аспектом, требующим улучшения. При необходимости связать и передать куда-то функцию в коде с высокой частотой вызова, от которого требуется не менее высокая производительность, первая мысль, которая возникает при использовании некой function, а не будет ли внутри аллокаций памяти?.. Безусловно, внутри function можно предусмотреть фиксированный буфер необходимого консервативного размера, использовать размещение по месту, не забыть про правильное выравнивание, но — есть другой путь.
Своя реализация
Для решения задачи используется язык C++11. Код проверен в среде Xcode 4.5.2. Реализация C++11 в Visual Studio 2012 запаздывает, но при установке November 2012 Compiler CTP можно получить необходимый для сборки примера уровень ее поддержки, хотя о полноценном практическом применении речи пока нет. В коде есть небольшие реверансы в сторону VS2012 для совместимости. При необходимости, решение можно переписать для C++03 ценой значительного увеличения объема кода, но сохраняя основные преимущества.
Начну с конца. Как может выглядеть надуманный пример использования:
#include <iostream> #include <vector> #include "function.hpp" int foo(int v) { std::cout << "foo: " << v << std::endl; return v; } struct bar { int baz(int v) { std::cout << "bar::baz: " << v << std::endl; return v; } }; int main() { // объявление синонима типа функции с необходимой сигнатурой вызова typedef bc::function<int (int)> function_ptr; std::vector<function_ptr> functions; // связывание со свободной функцией functions.push_back(BC_BIND(&foo)); // связывание с функцией-членом класса и сохранение контекста ее исполнения bar obj; functions.push_back(BC_BIND(&bar::baz, &obj)); int i = 0, c = 0; for (auto f : functions) c += f ? f(++i) : 0; std::cout << "result: " << c << std::endl; }
Видно, что в одном контейнере могут располагаться объекты, указывающие на разные свободные функции и функции-члены разных классов. Единственное требование к ним — единый формат параметров и тип возвращаемого значения. Из-за особенностей реализации, для более менее короткой и единообразной записи связывания используется макрос — BC_BIND. Интересно отметить, что макрос принимает разное количество аргументов, один и два, но препроцессор не поддерживает перегрузку одноименных макросов по количеству аргументов, однако, он поддерживает передачу переменного количества аргументов через эллипсис, что в совокупности с некоторой магией позволяет добиться симуляции перегрузки по количеству аргументов. Не буду углубляться далее, самостоятельно разобраться при желании совсем не сложно, только замечу, что Visual C++ и тут проявился своим особым видением стандарта и для совместимости с его препроцессором потребовалась чуточку более сильная магия.
Повествование подходит к самому интересному. Как же можно единообразно хранить разные указатели на разные функции и функции-члены, да еще без динамических аллокаций? Стандарт не говорит чего-либо определенного про размер указателей на функции и их структуру, что позволило бы привести их к общему знаменателю. Если для указателей на свободные функции хотя бы POSIX требует их безопасного преобразования к void* и обратно, то для функций-членов ничего подобного нет. Тем не менее, выход есть. Пусть спектр обрабатываемых функций, которые требуется привести к единому интерфейсу вызова, в псевдокоде выглядит так:
function<return_type (...)> function_ptr; return_type free_function(...); class Class { return_type member_function(...); };
Похоже, но неодинаково. Преобразуем второй метод:
return_type member_function_wrapper(Class *context, ...) { return context->member_function(...); }
Уже лучше. Еще одна итерация:
return_type free_function_wrapper(void *unused, ...) { return free_function(...); } return_type member_function_wrapper(void *context, ...) { return static_cast<Class*>(context)->member_function(...); }
Отлично, обе функции, free_function_wrapper и member_function_wrapper имеют одинаковую сигнатуру. Если с первой вопросов быть не должно, то для второй осталось понять, как внести в ее контекст информацию о классе и сам указатель на функцию-член. И такая возможность тоже есть благодаря шаблонам, которые можно параметризовать не только типами и интегральными константами времени компиляции, но и адресами функций и функций-членов. Простой отвлеченный пример:
#include <iostream> struct bar { int baz(int v) { std::cout << "bar::baz: " << v << std::endl; return v; } }; template <typename Class, int (Class::*MemberFunctionPtr)(int)> int function_wrapper(void *self, int v) { return (static_cast<Class*>(self)->*MemberFunctionPtr)(v); } int main() { typedef int (*function_ptr)(void*, int); function_ptr f = &function_wrapper<bar, &bar::baz>; bar obj; int const i = f(&obj, 1); std::cout << "result: " << i << std::endl; }
Таким образом, в function достаточно хранить указатель на инстанцированную с необходимыми параметрами шаблонную функцию-обертку и указатель на контекст, который в случае функции-члена будет равен указателю на экземпляр объекта, в контексте которого должна исполняться функция, а иначе просто NULL. Никаких аллокаций памяти, тривиальные конструктор копирования и оператор присваивания — по-моему, здорово.
В заключение осталось привести исходный текст хидера с реализацией из первого примера. Построчно разбирать его смысла не вижу, основная идея обозначена. Отмечу, что обработка разного количества параметров функций реализована при помощи шаблонов с переменным количеством аргументов из C++11 и именно это потребует больше всего дополнительного кода в случае переноса на С++03.
#pragma once //#define BC_NO_EXCEPTIONS #include <utility> #include <functional> #define BC_SUBST(Arg) Arg #define BC_BIND_DISAMBIGUATE2(has_args, ...) BC_SUBST(BC_BIND_ ## has_args (__VA_ARGS__)) #define BC_BIND_DISAMBIGUATE(has_args, ...) BC_BIND_DISAMBIGUATE2(has_args, __VA_ARGS__) #define BC_HAS_ARGS_IMPL(TWO, ONE, N, ...) N #define BC_HAS_ARGS(...) BC_SUBST(BC_HAS_ARGS_IMPL(__VA_ARGS__, 2, 1, ERROR)) #define BC_BIND(...) BC_BIND_DISAMBIGUATE(BC_HAS_ARGS(__VA_ARGS__), __VA_ARGS__) #define BC_BIND_1(fp) bc::detail::bind<decltype(fp), fp>() #define BC_BIND_2(mf, ip) bc::detail::bind<decltype(mf), mf>(ip) namespace bc // bicycle { template <typename Signature> class function; namespace detail { template <typename Signature> struct function_traits; template <typename ReturnType, typename ...ArgumentTypes> struct function_traits<ReturnType (*)(ArgumentTypes...)> { //typedef ReturnType (*Signature)(ArgumentTypes...); // MS error C3522: parameter pack cannot be expanded in this context typedef function<ReturnType (ArgumentTypes...)> function_type; template <typename Signature, Signature fp> static ReturnType wrapper(void const *, ArgumentTypes&& ... args) { return (*fp)(std::forward<ArgumentTypes>(args)...); } }; template <typename ReturnType, typename Class, typename ...ArgumentTypes> struct function_traits<ReturnType (Class::*)(ArgumentTypes...)> { //typedef ReturnType (Class::*Signature)(ArgumentTypes...); // MS error C3522: parameter pack cannot be expanded in this context typedef Class * class_ptr; typedef function<ReturnType (ArgumentTypes...)> function_type; template <typename Signature, Signature mf> static ReturnType wrapper(const void *ip, ArgumentTypes&& ... args) { Class* instance = const_cast<Class*>(static_cast<Class const *>(ip)); return (instance->*mf)(std::forward<ArgumentTypes>(args)...); } }; template <typename ReturnType, typename Class, typename ...ArgumentTypes> struct function_traits<ReturnType (Class::*)(ArgumentTypes...) const> { //typedef ReturnType (Class::*Signature)(ArgumentTypes...) const; // MS error C3522: parameter pack cannot be expanded in this context typedef const Class * class_ptr; typedef function<ReturnType (ArgumentTypes...)> function_type; template <typename Signature, Signature mf> static ReturnType wrapper(void const *ip, ArgumentTypes&& ... args) { Class const *instance = static_cast<Class const *>(ip); return (instance->*mf)(std::forward<ArgumentTypes>(args)...); } }; // bind free function template <typename Signature, Signature fp> typename function_traits<Signature>::function_type bind() { typedef function_traits<Signature> traits; return typename traits::function_type(&traits::template wrapper<Signature, fp>, 0); } // bind member function template <typename Signature, Signature mf> typename function_traits<Signature>::function_type bind(typename function_traits<Signature>::class_ptr ip) { typedef function_traits<Signature> traits; return typename traits::function_type(&traits::template wrapper<Signature, mf>, ip); } } template <typename ReturnType, typename ...ArgumentTypes> class function<ReturnType (ArgumentTypes...)> { typedef ReturnType (*StaticFuncPtr)(void const*, ArgumentTypes&& ...); public: function() : func_(0), data_(0) {} function(StaticFuncPtr f, void const *d) : func_(f), data_(d) {} ReturnType operator () (ArgumentTypes... args) const { #ifndef BC_NO_EXCEPTIONS if (!func_) throw std::bad_function_call(); #endif // BC_NO_EXCEPTIONS return (*func_)(data_, std::forward<ArgumentTypes>(args)...); } explicit operator bool() const { return 0 != func_; } bool operator == (function const &other) const { return func_ == other.func_ && data_ == other.data_; } bool operator != (function const &other) const { return !(*this == other); } private: StaticFuncPtr func_; void const *data_; }; }
ссылка на оригинал статьи http://habrahabr.ru/post/166589/
Добавить комментарий