Продолжаем изобретать function

в 8:28, , рубрики: c++, c++11, function, метки: , ,

Под влиянием предыдущей статьи предлагаю продолжить тему создания собственной реализации такой полезной идиомы, как 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.

function.hpp

#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

Источник

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


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