Пишем свой std::function (boost::function)

от автора

Классы std::function и boost::function являются высокоуровневыми обертками над функциями и функциональными объектами. Объекты таких классов позволяют хранить и вызывать функции и функторы с заданной сигнатурой, что бывает удобно, например, при создании callback вызовов (например, мы можем регистрировать несколько обработчиков, и это могут быть как обычные функции, так и объекты с определенным оператором =)

Если вам интересно, каким образом реализуется данный функционал, то прошу под кат

Краткое введение, примеры использования

Если вы не знакомы с 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/


Комментарии

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

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