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