1,6
рейтинг
20 ноября 2012 в 18:55

Разработка → Пишем свой 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 имеет шаблонный конструктор, который принимает один аргумент, в этом конструкторе создается наследник free_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
карма
57,0
рейтинг 1,6
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (36)

  • –9
    Мне очень интересно — для кого эта статья и какое ее практическое применение?
    • +20
      Для программистов C++, которые хотят знать, как устроены некоторые части стандартной библиотеки.

      p.s. Вообще здесь недавно был опрос, в котором большинство высказалось за более сложные «технические» статьи. На мой взгляд — это как раз такая статья, так как затрагивает довольно тонкие моменты C++
      • –4
        > Вообще здесь недавно был опрос
        И где же?

        По моим наблюдениям так тут всем наоборот сложные статьи ну вообще не по душе. (посмотрите например комментарии к этому и этому)
        • +4
          А воспользоваться поиском религия не позволяет?

          И что вообще за идиотское замечание? Вы статьи пишите для рейтинга и кармы?
          Тот факт, что народ плюсует всякий шлак (новости, истории, тренинги) — проблема именно народа. Такие статьи не несут никакой полезной нагрузки, в отличие от той, что представлена в данном топике.
          • –2
            Долбойобы…
            нет, не для рейтинга и кармы… согласен с вами сто раз.
          • –3
            > А воспользоваться поиском религия не позволяет
            идите на*уй, вопрос был риторический…
      • +1
        >>которые хотят знать
        Это похвально, только Вы не озвучили целевую аудиторию и какую проблему аудитории хотите решить и чем помочь ей.

        Я также как и Вы считаю, что всегда имеет смысл подстраиваться под более умных товарищей, а для этого у любого сейчас можно спрашивать ссылку на githum\bitbucket.
  • –3
    По моему для тех, кто хочет разобраться в STL есть исходный код.
    • +7
      Лично мне тяжело читать исходный код STL из-за их соглашения об именовании внутренних переменных, методов (чтобы не допустить пересечения имен с макросами, определенными пользователем). Где-то проскакивала ссылка, о том чтобы изменить ситуацию, но пока что все остается так как есть.

      К тому же в полноценных реализациях много вспомогательного кода: debug assertions, обработка пограничных ситуаций и пр. Мне кажется, что нужно рассказывать именно общую идею (плюс несколько ключевых особенностей), и тогда станет проще понимать уже полноценные реализации
      • –6
        Если программист сам не в состоянии понять общую идею любой концепции — то и ваша статья ему мало поможет, в худшем случае он будет как обезьяна с гранатой пихать свою собственную (читай — копипаст с этой статьи) реализацию куда нужно и куда не нужно, в лучшем — захавает и завтра забудет.
        • +7
          Обезьян с гранатой здесь рассматривать неуместно, не дай бог, прочитают про ручное управление памятью, или возможность перегрузки глобальных операторов new/delete. Мне, например, всегда было интересно, как реализованы разные вещи в программировании. Другое дело, что не всегда хватает уровня знаний, например, я бы эту статью года 3-4 назад просто бы не понял.

          Лично мне очень нравится копаться в реализации, выискивать какие-то паттерны, приемы программирования, затем обобщать эти данные. Когда разбираешь разные, казалось бы, несвязанные друг с другом вещи, а потом понимаешь, что в их основе лежит общая концепция, которой лет 50 от роду (привет LISP), вот в этот момент и ловишь настоящий кайф от проделанной работы
    • –3
      Тот код написан для компилятора, а не для человека.
      • +2
        По моему мнению, код всегда пишется для человека, что, разработчики libstdc++ не люди что-ли? Да, этот код изменяет и поддерживает гораздо меньше людей, и нужна более высокая квалификация, чтобы вносить в него изменения, но это не отменяет того факта, что когда-то его будут перечитывать, поддерживать. К сожалению, я не могу найти ссылку, где описывалось подробно, почему было выбрано такое соглашение об именовании.

        Кстати, если открыть исходный код того же std::function, то можно увидеть кучу doxygen комментариев, а они то компилятору уж точно бесполезны.
        • –1
          В теории — конечно, для человека всё пишется. Однако, это не делает автоматически код из заголовочных файлов реализации Стандартной библиотеки для GСС (или MS компилятора) удобным или лёгким для чтения.

          Пытаться «учиться» по нему, или «изучать паттерны», конечно, можно. Только зачем это другим советовать?
          • 0
            Я изначально спросил — для кого эта статья?

            Для пользователей stl::function? Вряд-ли.
            Для написания своего собственного лунного модуля? Если для этого — то если человек не может без этой статьи осознать, как реализовать — хорошего эта статья не принесет, а если способен — то она ему не нужна (он сам посмотрит в исходный код за деталями).
            Для общего развития? На Хабре и в инетрнетах и так полно статей для общего развития. Представьте, если все сейчас начнут сюда сливать свои измышления по поводу тех или иных концепций.
            • +1
              А вы пробовали читать исходники STL? Там рассматривается куча частных случаев, которые позволяют выиграть в скорости по сравнению с более общим решением, иногда встречаются оптимизации под конкретные платформы. Например std::copy реализован с помощью десятка вспомогательных шаблонных классов.
              Это не так просто читается и это не годится в качестве обучающего материала.
              • +1
                Да, я читал исходники STL. Но! Только когда писал свою реализацию под конкретную платформу, под конкретные задачи. До этого достаточно было понимать что те или иные контейнеры должны делать. Если лезть разбираться, какая реализация стоит за каждым интерфейсом — жизни не хватит. Не сомневаюсь, что большинство знает, как работает map, даже знает, чем AVL-дерево отличается от красно-черного. Но я очень сомневаюсь, что если я выложу пост с заголовком «Пишем свой std::map», он будет кому-то нужен.
          • 0
            Я вроде и не призывал курить исходный код stl, я лишь выразил свое сожаление по поводу их coding style guide. Кроме этого я добавил, что мне самому интересно ковыряться в исходниках, но это тоже на призыв никак не тянет.
            • 0
              Призывал antonyter, ему я и писал, что неразумно так делать
        • +1
          А что вас напрягает в именах? __ в начале или использование строчных букв и _ для разделения слов? Имена начинающиеся с __ зарезервированы стандартом под нужды компилятора и стандартной библиотеки. Т.е. такое именование фактически диктуется стандартом.
          • 0
            Небольшое уточнение: зарезервированы имена, содержашие два подчёркивания подряд где угодно (а не только в начале). C++11 [global.names] p1
    • НЛО прилетело и опубликовало эту надпись здесь
      • +1
        Совершенно не понял, что вы пытались сказать.
        • НЛО прилетело и опубликовало эту надпись здесь
          • +1
            Вы уж извините, но мне кажется, что, судя по тому, как вы строите предложения, ваш код, скорее всего, для сторонних разработчиков выглядит как тихий ужас. А это так, Маяковский чихнул.
    • +4
      Да, а я для тех, кто хочет разобраться в C++, есть стандарт. И зачем только люди пишут книги?

      Что за бред?
  • +3
    Раз уж используются variadic templates, то лучше использовать стандартный std::unique_ptr (С++11) вместо std::auto_ptr. Ещё, наверное, можно попытаться избежать лишних копирований (move semantics).
    • 0
      Или shared_ptr, тогда лишних копирований не будет и не придется писать оператор присваивания и копирующий конструктор. Вот только непонятно, во всех ли случаях (например, std::bind) это будет правильно работать.
  • 0
    Простите, не понял как происходит работа с функторами, или ваша реализация их не поддерживает?
    • 0
      Полностью поддерживает, можно создать функтор с помощью std::bind, или написать лямбда функцию. Все это полностью покрывается шаблонным конструктором, в котором создается объект класса free_function_holder.

      В этом и заключается вся прелесть механизма type erasure, что он позволяет спрятать любую сущность за единым интерфесом, лишь бы у него совпадала сигнатура вызываемой функции
  • +1
    Спасибо, с этим разобрался. Еще момент, работает ли присваивание функций/функторов?
    Вы уж простите за такие вопросы, из примеров непонятно, а сходу по коду сообразить не могу.
    • 0
      С присваиванием накладка вышла, я забыл написать реализацию для произвольного типа, в любом случае, она будет тривиальна. Вам спасибо за то что нашли ошибку!
      • 0
        Кстати, я написал следующий тест
        void check4()
        {
        	typedef function<int (void)> int_function_t;
        
        	int_function_t f1;
        
        	Bar bar;
        
        	f1 = std::bind(&Bar::callMe, bar, 10);
        	cout << "calling binded member function with signature int (void): " <<  f1() << endl;
        }

        И он успешно выполнился. Здесь произошла следующая последовательность действий:
        Сначала вызвался конструктор без аргументов
        Затем создался временный объект с помощью конструктора free function (в качестве параметра был передан созданный с помощью std::bind объект)
        Затем вызвался оператор присваивания.

        Если пометить конструктор free function как explicit, то мы увидим следующую ошибку:
        error: no match for ‘operator=’ in ‘f1 = std::bind(_Func&&, _BoundArgs&& ...) [with _Func = int (Bar::*)(int); _BoundArgs = {Bar&, int}; typename std::_Bind_helper<std::__is_socketlike<_Func>::value, _Func, _BoundArgs ...>::type = std::_Bind<std::_Mem_fn<int (Bar::*)(int)>(Bar, int)>]((* & bar), (* &10))’


        Так что, в принципе все должно работать
  • 0
    А чем std::function и аналоги лучше, чем простой указатель на функцию? Его тоже можно копировать, приравнивать к разным функциям и вызывать эти функции с его помощью.
    • +1
      Но ему нельзя присвоить функтор, а иногда надо иметь состояние.
    • +1
      К примеру, с помощью функтора+bind можно в одном месте создать его (и передать все нужные ему параметры), а в другом месте — вызвать.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.