Продолжаем изобретать function
Попробую дать краткую вольную формулировку: это объект, обладающий семантикой значения и позволяющий привести свободные функции и функции-члены классов к единому интерфейсу вызова. При необходимости, объект сохраняет контекст вызова, речь про this, что актуально для функций-членов классов.
Продвинутые реализации, вроде boost, предлагают такие интересные средства, как адаптирование функций с неподходящей сигнатурой под требуемый интерфейс вызова, а также возможность связывания аргументов с параметрами в момент инициализации. Но, на мой взгляд, подобные возможности лежат за рамками основной темы, заметно раздувают реализацию, но, при этом, далеко не всегда необходимы. Такие средства безусловно удобны и даже иногда полезны, но, по моему субъективному мнению, усложняют чтение и отладку кода. Пусть собственная реализация будет в чем-то проще, обладая при этом такими преимуществами, как, относительная лаконичность и простота.
Мотивация к продолжению поиска
Итак, постановка задачи: требуется класс, параметризуемый сигнатурой функции, определяющей интерфейс вызова, позволяющий связывать и позднее вызывать как свободные функции, так и функции-члены классов, сохраняя для последних контекст вызова. Возвращаясь к решению из предыдущей статьи, отвечает ли оно указанным требованиям, какие у него можно заметить недостатки, поддающиеся исправлению? Решение не сохраняет контекст вызова для функций-членов классов и, таким образом, теряет в универсальности применения. Внутри используются виртуальные функции, наследование и динамическое распределение памяти, что, как будет видно дальше, не является необходимым, можно сделать проще. Оговорюсь, что динамическое распределение памяти считаю основным аспектом, требующим улучшения. При необходимости связать и передать куда-то функцию в коде с высокой частотой вызова, от которого требуется не менее высокая производительность, первая мысль, которая возникает при использовании некой function, а не будет ли внутри аллокаций памяти. Безусловно, внутри function можно предусмотреть фиксированный буфер необходимого консервативного размера, использовать размещение по месту , не забыть про правильное выравнивание, но — есть другой путь.
Своя реализация
Для решения задачи используется язык C++11. Код проверен в среде Xcode 4.5.2 . Реализация C++11 в Visual Studio 2012 запаздывает, но при установке November 2012 Compiler CTP можно получить необходимый для сборки примера уровень ее поддержки, хотя о полноценном практическом применении речи пока нет. В коде есть небольшие реверансы в сторону VS2012 для совместимости. При необходимости, решение можно переписать для C++03 ценой значительного увеличения объема кода, но сохраняя основные преимущества.
Начну с конца. Как может выглядеть надуманный пример использования:
#include #include #include "function.hpp" int foo(int v) < std::cout struct bar < int baz(int v) < std::cout >; int main() < // объявление синонима типа функции с необходимой сигнатурой вызова typedef bc::functionfunction_ptr; std::vector 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
Видно, что в одном контейнере могут располагаться объекты, указывающие на разные свободные функции и функции-члены разных классов. Единственное требование к ним — единый формат параметров и тип возвращаемого значения. Из-за особенностей реализации, для более менее короткой и единообразной записи связывания используется макрос — BC_BIND. Интересно отметить, что макрос принимает разное количество аргументов, один и два, но препроцессор не поддерживает перегрузку одноименных макросов по количеству аргументов, однако, он поддерживает передачу переменного количества аргументов через эллипсис, что в совокупности с некоторой магией позволяет добиться симуляции перегрузки по количеству аргументов. Не буду углубляться далее, самостоятельно разобраться при желании совсем не сложно, только замечу, что Visual C++ и тут проявился своим особым видением стандарта и для совместимости с его препроцессором потребовалась чуточку более сильная магия.
Повествование подходит к самому интересному. Как же можно единообразно хранить разные указатели на разные функции и функции-члены, да еще без динамических аллокаций? Стандарт не говорит чего-либо определенного про размер указателей на функции и их структуру, что позволило бы привести их к общему знаменателю. Если для указателей на свободные функции хотя бы POSIX требует их безопасного преобразования к void* и обратно, то для функций-членов ничего подобного нет. Тем не менее, выход есть. Пусть спектр обрабатываемых функций, которые требуется привести к единому интерфейсу вызова, в псевдокоде выглядит так:
function 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(context)->member_function(. ); >
Отлично, обе функции, free_function_wrapper и member_function_wrapper имеют одинаковую сигнатуру. Если с первой вопросов быть не должно, то для второй осталось понять, как внести в ее контекст информацию о классе и сам указатель на функцию-член. И такая возможность тоже есть благодаря шаблонам, которые можно параметризовать не только типами и интегральными константами времени компиляции, но и адресами функций и функций-членов. Простой отвлеченный пример:
#include struct bar < int baz(int v) < std::cout >; template int function_wrapper(void *self, int v) < return (static_cast(self)->*MemberFunctionPtr)(v); > int main() < typedef int (*function_ptr)(void*, int); function_ptr f = &function_wrapper; bar obj; int const i = f(&obj, 1); std::cout
Таким образом, в function достаточно хранить указатель на инстанцированную с необходимыми параметрами шаблонную функцию-обертку и указатель на контекст, который в случае функции-члена будет равен указателю на экземпляр объекта, в контексте которого должна исполняться функция, а иначе просто NULL. Никаких аллокаций памяти, тривиальные конструктор копирования и оператор присваивания — по-моему, здорово.
В заключение осталось привести исходный текст хидера с реализацией из первого примера. Построчно разбирать его смысла не вижу, основная идея обозначена. Отмечу, что обработка разного количества параметров функций реализована при помощи шаблонов с переменным количеством аргументов из C++11 и именно это потребует больше всего дополнительного кода в случае переноса на С++03.
function.hpp
#pragma once //#define BC_NO_EXCEPTIONS #include #include #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() #define BC_BIND_2(mf, ip) bc::detail::bind(ip) namespace bc // bicycle < template class function; namespace detail < template struct function_traits; template struct function_traits < //typedef ReturnType (*Signature)(ArgumentTypes. ); // MS error C3522: parameter pack cannot be expanded in this context typedef functionfunction_type; template static ReturnType wrapper(void const *, ArgumentTypes&& . args) < return (*fp)(std::forward(args). ); > >; template struct function_traits < //typedef ReturnType (Class::*Signature)(ArgumentTypes. ); // MS error C3522: parameter pack cannot be expanded in this context typedef Class * class_ptr; typedef functionfunction_type; template static ReturnType wrapper(const void *ip, ArgumentTypes&& . args) < Class* instance = const_cast(static_cast(ip)); return (instance->*mf)(std::forward(args). ); > >; template struct function_traits < //typedef ReturnType (Class::*Signature)(ArgumentTypes. ) const; // MS error C3522: parameter pack cannot be expanded in this context typedef const Class * class_ptr; typedef functionfunction_type; template static ReturnType wrapper(void const *ip, ArgumentTypes&& . args) < Class const *instance = static_cast(ip); return (instance->*mf)(std::forward(args). ); > >; // bind free function template typename function_traits::function_type bind() < typedef function_traitstraits; return typename traits::function_type(&traits::template wrapper, 0); > // bind member function template typename function_traits::function_type bind(typename function_traits::class_ptr ip) < typedef function_traitstraits; return typename traits::function_type(&traits::template wrapper, ip); > > template class function < 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(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_; >; >
Функции в C++: руководство для начинающих

Привет, дорогой читатель! Вам, наверное, приходилось в программе использовать один и тот же блок кода несколько раз. Например, выводить на экран одну и ту же строчку. Для того, чтобы каждый раз не писать одинаковый блок кода в C++, присутствуют функции.
Сегодня мы разберем, что такое функции и как правильно их использовать в своей программе. Поехали!
Что такое функции
Функции - это блок кода, который вы можете использовать в любом участке вашей программы неограниченное количество раз. Например, в программе ниже мы выводим 2 строки (без применения функций):
#include using namespace std; int main() cout <"Функция очень хороший инструмент в программировании"; cout <"С помощью его можно улучшить свой уровень программирования"; system ("pause") return 0; >
А вот если бы мы использовали функции, то у нас получилось бы так:
#include using namespace std; void func () // функция cout <"Функция очень хороший инструмент в программировании"; cout <"С помощью его можно улучшить свой уровень программирования"; > int main() func(); // вызов функции system ("pause") return 0; >
Мы хотим, чтобы вы обратили внимание на увеличение количества строк в первой программе при выводе этих двух строк 5 раз.
Как видите, если правильно применять функции, то можно уменьшить программу в несколько раз. Но вы должны помнить - бессмысленно использовать функции без видимых оснований (например, если логика внутри функции слишком специфична).
Вашему компилятору будет совершенно без разницы, использовали вы функции или несколько раз вставили одинаковый блок кода, в итоге он выведет одинаковый результат.
Чтобы понять, как работают локальные переменные (например, переменные в функциях) и глобальные переменные, можете почитать данную статью.
Как создать функции в C++
Таким образом, чтобы создать функции, нужно использовать конструкцию, которая находится пониже:
тип данных, который будет возвращаться функцией> имя> (аргументы функции>) блок кода > >
Давайте разберем эту конструкцию:
- Тип данных функции. В самом начале нам нужно указать тип данных, который в конечном итоге будет передавать функция.
Но если мы не собираемся ничего передавать, а например, хотим просто вывести строку, то на месте можно указать тип void . Также вы можете указать тип int, это ни на что не повлияет.
void stroka() cout <"Выводим строку без всяких переменных"; >
- Имя функции. Нам нужно задать функции имя (исключениями являются зарезервированные слова в C++, имена начинающиеся с цифр, а также имена разделенные пробелом).
Лучше всего задавать такое имя, которое будет говорить вам в будущем, за что отвечает эта функция. И тогда в будущем вам не придется вспоминать, за что она отвечает.
- Аргументы функции. В скобках (после имени функции) могут находиться аргументы функции. Аргумент функции - это значение, которое можно передать функции при ее вызове. Если аргумент функции не один, а их несколько, то их нужно разделять запятой.
int sum(int b, int c) // у нас аргументы функции это b и c
Если аргументов в функции нет, то в скобках можно указать тип void . Но писать его необязательно, он стоит по умолчанию.
void stroka(void) cout <"Просто выводим строку"; >
- Блок кода. После открывающей скобки идет блок кода, который будет начина