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

в 14:55, , рубрики: boost, c++, template metaprogramming, Программирование, метки: , ,

Классы 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). Грубо говоря, реализация сделана для функций с одним аргументом, а потом «клонирована» на большее количество аргументов с помощью препроцессорной магии.

Полностью пример находится здесь

Автор: prograholic

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js