Продолжаем изобретать function

от автора

Под влиянием предыдущей статьи предлагаю продолжить тему создания собственной реализации такой полезной идиомы, как function в C++, и рассмотреть некоторые аспекты ее использования.

Что такое 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.

function.hpp

#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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *