Если вам интересно, каким образом реализуется данный функционал, то прошу под кат
Краткое введение, примеры использования
Если вы не знакомы с boost::function и std::function, то можно ознакомиться с ними здесь и здесь
std::function входит в стандарт языка c++11, и компиляторы gcc-4.7 и msvc-2012 его поддерживают (возможно, более ранние версии тоже имеют поддержку). В принципе, в рамках этой статьи можно считать реализацию от boost и стандартную полностью идентичными, так что пользоваться можно любой из реализаций
Собственно пример использования:
int func1() { return 0; } struct callable { int operator() () { return 1; } }; ///... boost::function<int (void)> x; x = func1; int res = x(); // вернет 0 в качестве результата callable c; x = c; res = x(); // вернет 1 в качестве результата
Переходим к самой реализации
Саму реализацию будем делать в несколько этапов:
- Самый простой use-case — инициализировать наш объект указателем на функцию или объектом-функцией и вызвать оператор ()
- Поддержка операторов присваивания и копирующего конструктора
- Поддержка указателей на функции-члены
Самая простая реализация, понятие Type erasure
В основе реализации данного класса лежит паттерн Type Erasure, более доступно можно почитать здесь Его предназначение заключается в том, что мы можем «спрятать» за одним интерфейсом различные сущности (объекты, указатели и пр.), которые предоставляют сходные возможности (например, осуществить вызов функции с тремя аргументами). Type erasure также можно представить как мостик, который связывает полиморфизм времени исполнения (runtime polymorfism) и полиморфизм времени компиляции (compile-time polymorfism).
Итак, переходим к реализации.
Мы будем использовать variadic templates из стандарта C++11. Например, gcc поддерживает этот функционал аж с версии 4.3, поэтому можно смело им пользоваться.
Не будем оригинальны и назовем наш класс function. Очевидно, что класс будет шаблонный, также очевидно, что у него будет один параметр шаблона — сигнатура (тип) вызываемой функции. Общая реализация шаблона отсутствует, вся работа будет происходить в частичной специализации шаблона. Частичная специализация нужна для того чтобы мы могли использовать типы аргументов и возвращаемого значения из нашей сигнатуры.
Сама реализация:
template <typename UnusedType> class function; template <typename ReturnType, typename ... ArgumentTypes> class function <ReturnType (ArgumentTypes ...)> { public: function() : mInvoker() {} template <typename FunctionT> function(FunctionT f) : mInvoker(new free_function_holder<FunctionT>(f)) {} ReturnType operator ()(ArgumentTypes ... args) { return mInvoker->invoke(args ...); } private: class function_holder_base { public: function_holder_base() {} virtual ~function_holder_base() {} virtual ReturnType invoke(ArgumentTypes ... args) = 0; }; typedef std::auto_ptr<function_holder_base> invoker_t; template <typename FunctionT> class free_function_holder : public function_holder_base { public: free_function_holder(FunctionT func) : function_holder_base(), mFunction(func) {} virtual ReturnType invoke(ArgumentTypes ... args) { return mFunction(args ...); } private: FunctionT mFunction; }; invoker_t mInvoker; };
Класс function определяет оператор () соответствующий сигнатуре функции и передает управление методу invoke у класса function_holder_base. Этот класс имеет виртуальную функцию invoke, которая тоже совпадает с указанной сигнатурой (за исключением неявного параметра this).
Также класс function имеет шаблонный конструктор, который принимает один аргумент, в этом конструкторе создается наследник templated_function_holder класса function_holder_base. Этот наследник является шаблонным классом — он сохраняет в себе переданный аргумент (обычно это функтор или указатель на функцию). В нем также определяется метод invoke, который вызывает сохраненный функтор с заданными аргументами.
Здесь необходимо отметить несколько особенностей шаблонов в C++:
- Тип возвращаемого значения может быть любой (в том числе и void), то есть конструкция return mFunction(args …); валидна даже в том случае, когда возвращаемое значение void. Это сделано специально для шаблонного метапрограммирования, чтобы не плодить специализации для типа void.
- Аналогичным образом работают и variadic templates если количество входных аргументов равно 0 (функция без параметров).
- Мы можем написать шаблонный класс, который является наследником какого-то базового класса (причем необязательно шаблонного) — на этом стоит весь паттерн Type erasure
В принципе, мы получили работоспособный аналог std::function и boost::function, мы вполне можем писать следующий код:
int func2(const int * x, int y) { return (*x) + y; } ///... typedef function<int (const int * , int)> int_function_with_two_args_t; int_function_with_two_args_t f2(func2); int x = 10; cout << "calling function with signature int (const int * , int): " << f2(&x, 20) << endl;
Переходим к улучшениям нашего класса:
Копируем поведение обычного указателя на функцию — оператор присваивания и конструктор копирования
Для того чтобы скопировать или присвоить объект мы должны уметь копировать (клонировать) указатель на базовый класс function_holder_base. Для этого расширим интерфейс этого класса следующим образом:
class function_holder_base { public: function_holder_base() {} virtual ~function_holder_base(){} virtual ReturnType invoke(ArgumentTypes ... args) = 0; virtual std::auto_ptr<function_holder_base> clone() = 0; private: function_holder_base(const function_holder_base & ); void operator = (const function_holder_base &); };
Делаем наш класс некопируемым (декларируем соответствующие оператор и конструктор в private секции), и добявляем метод clone, чтобы наследники сами определяли правильную стратегию клонирования.
Также, в класс function добавляются операторы присваивания и конструктор копирования:
function(const function & other) : mInvoker(other.mInvoker->clone()) {} function & operator = (const function & other) { mInvoker = other.mInvoker->clone(); }
Здесь мы используем auto_ptr и его разрушающее присваивание.
Осталось написать реализацию метода clone в наследнике — free_function_holder:
typedef free_function_holder<FunctionT> self_type; virtual invoker_t clone() { return invoker_t(new self_type(mFunction)); }
Вот собственно и все, теперь наш класс ведет себя как обычный указатель на функцию, и мы можем делать так:
int func1() { return 0; } ///... typedef function<int (void)> int_function_t; int_function_t f1(func1); cout << "calling function with signature int (void): " << f1() << endl; int_function_t f2; f2 = f1; cout << "calling function after assignment operator with signature int (void): " << f2() << endl; int_function_t f3(f2); cout << "calling function after copying ctor with signature int (void): " << f3() << endl;
Переходим к заключительной части:
Добавляем поддержку указателей на функции-члены
Поддержка указателей на методы у меня получилась довольно ограниченная: можно передавать объект к которому применяется указатель на метод только по значению (а хотелось бы еще по ссылке (const и не const) и по указателю (const и не const)), но в принципе для примера этого будет достаточно.
При использовании boost (std) и нашей реализации function придерживаемся правила, что первым аргументом должен идти объект, к которому применяется указатель на метод, соответственно аргументы теперь делятся на два вида: сам объект, и аргументы метода. Соответственно, у нас есть гарантия, что количество аргументов строго больше 0, этим мы и воспользуемся далее:
template <typename FunctionType, typename ClassType, typename ... RestArgumentTypes> class member_function_holder : public function_holder_base { public: typedef FunctionType ClassType::* member_function_signature_t; member_function_holder(member_function_signature_t f) : mFunction(f){} virtual ReturnType invoke(ClassType obj, RestArgumentTypes ... restArgs) { return (obj.*mFunction)(restArgs ...); } virtual invoker_t clone() { return invoker_t(new member_function_holder(mFunction)); } private: member_function_signature_t mFunction; };
Реализация конструктора, который принимает указатель на метод тривиальна:
template <typename FunctionType, typename ClassType> function(FunctionType ClassType::* f) : mInvoker(new member_function_holder<FunctionType, ArgumentTypes ...>(f)) {}
Мы сделали еще одного наследника function_holder_base специально для указателей на методы. Здесь используется особенность variadic templates: переменное количество типов (которое задается троеточием) можно расщепить на фиксированную часть, и остаток переменной длины (в котором количество типов меньше на размер фиксированной части). В конструкторе мы передаем в параметры шаблона фиксированную часть состоящую из одного типа — сигнатура функции, и произвольную часть — все остальное (это типы всех аргументов), а в реализации member_function_holder мы требуем, чтобы фиксированная часть состояла из двух элементов — сигнатура функции, класс, в котором находится наш метод, и аргументы непосредственно для вызова (здесь мы как раз используем указанную выше гарантию, что количество всех аргументов строго больше 0). Таким образом, мы сохраняем указатель на метод в конструкторе, и вызываем его в реализации метода invoke.
Отдельно хочется сказать про крайне неудобный и неинтуитивный способ декларирования типа «указатель на метод» и про способ вызова такого метода. По этому поводу в C++ FAQ есть предупреждение, о том как минимизировать количество головной боли при работе с указателями на методы (я полчаса гуглил, как записать правильно это выражение)
Вот и пример использования:
struct Foo { int smth(int x) { return x + 1; } }; ///... typedef function<int (Foo, int)> member_function_t; member_function_t f1 = &Foo::smth; Foo foo; cout << "calling member function with signature int (int): " << f1(foo, 5) << endl;
Заключение
Благодаря поддержке variadic templates реализация function получилась довольно лаконичной, в том же boost из-за необходимости поддерживать старые компиляторы (в которых нет variadic templates) данный функционал реализован с помощью boost.preprocessor (и имеет ограничение на количество аргументов — по умолчанию 10, можно поменять, определив соответствующий define: BOOST_FUNCTION_MAX_ARGS). Грубо говоря, реализация сделана для функций с одним аргументом, а потом «клонирована» на большее количество аргументов с помощью препроцессорной магии.
Полностью пример находится здесь
ссылка на оригинал статьи http://habrahabr.ru/post/159389/
Добавить комментарий