Передаем указатели на функции-члены в C API

    Энное время назад в одной XMPP-комнате, посвященной C++, один посетитель спросил, нет ли какого способа в современных плюсах без лишнего кода передать указатель на функцию-член класса в качестве коллбека в C API. Ну, что-то вроде:

    // C API
    void doWithCallback (void (*fn) (int, void*), void *userdata);
    
    // C++ code
    struct Foo
    {
        void doFoo (int param);
    };
    
    int main ()
    {
        Foo foo;
        doWithCallback (MAGIC (/* &Foo::doFoo */), &foo);
    }
    

    Понятно, что в качестве MAGIC можно использовать свободную функцию, статическую функцию-член или вообще лямбду (2017-й год на дворе, всё-таки), но писать соответствующую конструкцию каждый раз для каждой функции руками несколько лениво, а препроцессор, как мы все, конечно, знаем — моветон.

    В этом посте мы попробуем (и, что характерно, у нас это получится) написать универсальную обёртку, а заодно посмотрим, как кое-какая фишка из C++17 поможет нам ещё сократить количество избыточного кода. Никаких крышесносных шаблонов здесь не будет, решение, на мой взгляд, достаточно тривиально, но, пожалуй, им всё равно имеет смысл поделиться (и заодно лишний раз попиарить новые возможности C++17).

    Сначала решим задачу попроще: предположим, что функция-член не принимает никаких аргументов (кроме неявного this, конечно, который нам и предстоит передать). Типичное лобовое решение тогда будет выглядеть примерно так:

    doWithCallback ([] (void *udata) { return static_cast<Foo*> (udata)->doFoo (); }, &foo);
    

    Черновик кода, от которого мы будем отталкиваться
    #include <iostream>
    
    void doWithCallback (void (*fn) (void*), void *userdata)
    {
        fn (userdata);
    }
    
    struct Foo
    {
        int m_i = 0;
        
        void doFoo ()
        {
            std::cout << m_i << std::endl;
        }
    };
    
    int main ()
    {
        Foo foo { 42 };
        doWithCallback ([] (void *udata) { return static_cast<Foo*> (udata)->doFoo (); }, &foo);
    }
    


    Если мы хотим сделать обобщённую обёртку, то, естественно, ей как-то надо передавать в точке создания коллбека, какую именно функцию над каким именно классом мы хотим вызвать. Кроме того, обёртке надо сохранить, собственно, что именно ей надо вызвать. Если с передачей всё просто (взяли указатель на функцию-член и передали), то сохранить его так просто не получится:

    код вроде:

    template<typename Ptr>
    auto MakeWrapper (Ptr ptr)
    {
        return [ptr] (void *udata) { return (static_cast<Foo*> (udata)->*ptr) (); };
    }
    
    int main ()
    {
        Foo foo { 42 };
        doWithCallback (MakeWrapper (&Foo::doFoo), &foo);
    }
    

    ожидаемо не соберётся, так как лямбды с непустым списком захвата нельзя преобразовать к сишному указателю на функцию:

    prog.cc:36:5: error: no matching function for call to 'doWithCallback'
        doWithCallback (MakeWrapper (&Foo::doFoo), &foo);
        ^~~~~~~~~~~~~~
    prog.cc:3:6: note: candidate function not viable: no known conversion from '(lambda at prog.cc:29:12)' to 'void (*)(void *)' for 1st argument
    void doWithCallback (void (*fn) (void*), void *userdata)
         ^
    

    Это имеет смысл: в сишном указателе на функцию недостаточно «места», чтобы кроме собственно указываемой функции сохранить ещё и некоторый контекст (или хотя бы указатель на него).

    Так что же делать? Мы обречены?

    Нет! На помощь приходят non-type template parameters: в подавляющем большинстве случаев при передаче коллбека мы на этапе компиляции знаем, какую именно функцию мы хотим вызвать, значит, мы можем параметризовать этим некоторый шаблон, и никакой информации в рантайме тащить с собой не придётся.

    Подобно тому, как мы можем параметризовать шаблон, скажем, целым числом, мы можем его параметризовать и указателем на функцию. Попробуем:

    template<typename R, typename C, R (C::*Ptr) ()>
    auto MakeWrapper ()
    {
        return [] (void *udata) { return (static_cast<C*> (udata)->*Ptr) (); };
    }
    
    int main ()
    {
        Foo foo { 42 };
        doWithCallback (MakeWrapper<void, Foo, &Foo::doFoo> (), &foo);
    }
    

    Работает! Но есть одна проблема: тип возвращаемого значения, тип класса и список типов параметров функции (в нашем случае он пустой) необходимо указывать каждый раз руками. А ведь лень же. Можем ли мы лучше?

    В C++11/14 мы можем заставить компилятор вывести вышеупомянутые типы, но для этого придётся указать желаемую функцию-член дважды: один раз для вывода типа переменной, соответствующей указателю на эту функцию, из которого мы уже сможем получить всё нужное, чтобы сформировать правильную «сигнатуру» для non-type-аргумента шаблона. Как-то так:

    template<typename T>
    struct MakeWrapperHelper
    {
        template<typename R, typename C>
        static R DetectReturnImpl (R (C::*) ());
    
        template<typename R, typename C>
        static C DetectClassImpl (R (C::*) ());
        
        template<typename U>
        using DetectReturn = decltype (DetectReturnImpl (std::declval<U> ()));
    
        template<typename U>
        using DetectClass = decltype (DetectClassImpl (std::declval<U> ()));
    
        using R = DetectReturn<T>;
        using C = DetectClass<T>;
    
        template<R (C::*Ptr) ()>
        auto Make ()
        {
            return [] (void *udata) { return (static_cast<C*> (udata)->*Ptr) (); };
        }
    };
    
    template<typename T>
    auto MakeWrapper (T)
    {
        return MakeWrapperHelper<T> {};
    }
    
    int main ()
    {
        Foo foo { 42 };
        doWithCallback (MakeWrapper (&Foo::doFoo).Make<&Foo::doFoo> (), &foo);
    }
    


    Полный код
    #include <iostream>
    #include <tuple>
    
    void doWithCallback (void (*fn) (void*), void *userdata)
    {
        fn (userdata);
    }
    
    struct Foo
    {
        int m_i = 0;
    
        void doFoo ()
        {
            std::cout << m_i << std::endl;
        }
    };
    
    template<typename T>
    struct MakeWrapperHelper
    {
        template<typename R, typename C>
        static R DetectReturnImpl (R (C::*) ());
    
        template<typename R, typename C>
        static C DetectClassImpl (R (C::*) ());
    
        template<typename U>
        using DetectReturn = decltype (DetectReturnImpl (std::declval<U> ()));
    
        template<typename U>
        using DetectClass = decltype (DetectClassImpl (std::declval<U> ()));
    
        using R = DetectReturn<T>;
        using C = DetectClass<T>;
    
        template<R (C::*Ptr) ()>
        auto Make ()
        {
            return [] (void *udata) { return (static_cast<C*> (udata)->*Ptr) (); };
        }
    };
    
    template<typename T>
    auto MakeWrapper (T)
    {
        return MakeWrapperHelper<T> {};
    }
    
    int main ()
    {
        Foo foo { 42 };
        doWithCallback (MakeWrapper (&Foo::doFoo).Make<&Foo::doFoo> (), &foo);
    }
    



    Но это всё выглядит страшновато и плохо пахнет. Можем ли мы лучше?

    Возможно, в рамках C++14 и можем, но я не придумал, как, а скорее нашёл доказательство, что этого сделать нельзя, но поля этой статьи слишком узки для него.

    Итак, основная проблема состоит в том, что мы обязаны явно указать тип non-type-аргумента шаблона, для чего нам нужно в том или ином виде явно же указать все эти типы возвращаемых значений и прочие подобные вещи. К счастью, в C++17 добавили ровно то, что нам нужно: автоматический вывод типа аргумента шаблона (работает пока только в разрабатываемых ветках clang и gcc). Искомый код существенно упрощается:

    template<typename R, typename C>
    C DetectClassImpl (R (C::*) ());
    
    template<auto T>
    auto MakeWrapper ()
    {
        using C = decltype (DetectClassImpl (T));
        return [] (void *udata) { return (static_cast<C*> (udata)->*T) (); };
    }
    
    int main ()
    {
        Foo foo { 42 };
        doWithCallback (MakeWrapper<&Foo::doFoo> (), &foo);
    }
    

    Всё.

    А, нет, не всё. Вспомним, что изначально наша функция-член могла принимать произвольный набор параметров. Аналогично можно определить DetectArgsImpl, возвращающий список типов аргументов:

    template<typename R, typename C, typename... Args>
    std::tuple<Args...> DetectArgsImpl (R (C::*) (Args...));
    

    и воспользоваться частичной специализацией для его разворачивания:

    template<auto, typename>
    struct MakeWrapperHelper;
    
    template<auto T, typename... Args>
    struct MakeWrapperHelper<T, std::tuple<Args...>>
    {
        auto operator() ()
        {
            using C = decltype (DetectClassImpl (T));
            return [] (Args... args, void *udata) { return (static_cast<C*> (udata)->*T) (args...); };
        }
    };
    
    template<auto T>
    auto MakeWrapper ()
    {
        return MakeWrapperHelper<T, decltype (DetectArgsImpl (T))> {} ();
    }
    

    Все вместе
    #include <iostream>
    #include <tuple>
    
    void doWithCallback (void (*fn) (int, void*), void *userdata)
    {
        fn (7831505, userdata);
    }
    
    struct Foo
    {
        int m_i = 0;
        
        void doFoo (int val)
        {
            std::cout << m_i << " vs " << val << std::endl;
        }
    };
    
    template<typename R, typename C, typename... Args>
    C DetectClassImpl (R (C::*) (Args...));
    
    template<typename R, typename C, typename... Args>
    std::tuple<Args...> DetectArgsImpl (R (C::*) (Args...));
    
    template<auto, typename>
    struct MakeWrapperHelper;
    
    template<auto T, typename... Args>
    struct MakeWrapperHelper<T, std::tuple<Args...>>
    {
        auto operator() ()
        {
            using C = decltype (DetectClassImpl (T));
            return [] (Args... args, void *udata) { return (static_cast<C*> (udata)->*T) (args...); };
        }
    };
    
    template<auto T>
    auto MakeWrapper ()
    {
        return MakeWrapperHelper<T, decltype (DetectArgsImpl (T))> {} ();
    }
    
    int main ()
    {
        Foo foo { 42 };
        doWithCallback (MakeWrapper<&Foo::doFoo> (), &foo);
    }
    


    Такие дела. Можно смело брать Tox API, libpurple API, gstreamer API, какое угодно сишное API и избегать кучи бойлерплейта.

    В качестве упражнения интересующемуся читателю можно добавить указание аргументов, которые передаются коллбеку сишным API, но которые нужно игнорировать — например, Tox первым аргументом передаёт указатель на себя же, который вполне может быть и так доступен в нашем плюсовом коде.

    А ещё хабрахабровской раскрашивалке синтаксиса от всего этого плохеет, кажется.
    Поделиться публикацией

    Похожие публикации

    Комментарии 33

      0
      Чето у меня C++14 версия не взлетела, куча ошибок (услал в личку). Не пойму как вы вызываете везде fn (7831505, userdata), если void doFoo (int val) (один параметр принимает).
        0

        Автор предполагает, что функция-член это обычная функция, только первым аргументом принимает увазатель this (это не всегда так). Правильно конечно же было бы использовать std::invoke.

          0

          Прошу игнорировать мой комментарий. Автор ничего такого не предполагал.

          0

          Это модификация изначального чернового кода, там doFoo вообще ничего не принимает, а сишная функция передаёт только userdata. В том коде, что вы мне прислали, это не так. Если интовый параметр убрать, то всё работает.


          Проблема в том, что функции, используемые для получения типа класса и возвращаемого значения, предполагают, что параметров у функции класса нет, поэтому такая страшная вереница ошибок.


          Обновил статью, чтобы прояснить этот момент.

          0
          Ожидал увидеть что-то вроде https://github.com/libffcall/libffcall
          Там в рантайме генерируется код для каждого использования, что позволяет передать замыкание туда, где ожидается обычная сишная функция, не предусматривающая пользовательских данных. Руки чешутся начать использовать, но уровень шаманства отпугивает.
            0

            Как-то это нетипобезопасно, истинно шаманисто и платформозависимо.


            Зато работало в 1995-м году, а не в 2017-м, это да.

            0
            Вот реализация для C++11. Используется тот факт, что первым параметром функции-члена передается this, поэтому пришлось поменять порядок агрументов у функции обратного вызова (fn) и соответственно doWithCallback(). Еще я дописал конструктор для Foo, без него почему-то не собиралось.

            #include <iostream>
            
            template< typename R, typename Class, typename... Args >
            auto make_wrapper( R (Class::*fn) (Args...) ) -> R(*)( void*, Args...)
            {
                auto tmp = (void*&)fn;
                return reinterpret_cast<R(*)( void*, Args... )>(tmp);
            }
            
            void doWithCallback (void (*fn) (void*, int), void *userdata)
            {
                fn( userdata, 7831505 );
            }
            
            struct Foo
            {
                int m_i = 0;
                Foo(int i) : m_i(i){}
            
                void doFoo (int val)
                {
                    std::cout << m_i << " vs " << val << std::endl;
                }
            };
            
            int main()
            {
                Foo foo = { 42 };
                doWithCallback(make_wrapper(&Foo::doFoo), &foo);
                return 0;
            }
            
              0
              поэтому пришлось поменять порядок агрументов у функции обратного вызова (fn)

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


              Ну и я почти уверен, что подобный каст — UB, но сходу не могу найти конкретную формулировку в стандарте, которая бы это запрещала.

                0
                «иногда порядок аргументов зафиксирован используемой библиотекой»
                А можно пример либы, где порядок аргументов в колбеке не зафиксирован?
                  0

                  Иногда просто вы контролируете и код, который принимает коллбеки, и поэтому вольны переупорядочить параметры как хотите.

                    0
                    Если код doWithCallback() ты пишешь сам, то сможешь поставить аргументы в вызове fn( userdata, 7831505 ); в нужном порядке. Если doWithCallback() ты не контролируешь, то придется писать обертку. В общем, если подумать внимательно, то порядок аргументов тут либо вообще не имеет значения, либо почти не имеет.
                    +2

                    Стандарт определяет, какие преобразования указателей разрешены, и среди них нет преобразования "указатель на функцию-член" -> "указатель на функцию". Более того, это точно не будет работать на MSVC (см thiscall)

                      0
                      Если стандарт чего-то не определяет, то не факт, что это UB, возможно, implementation defined или unspecified, а это большая разница. Но если учесть, что компиляторы пишут люди для людей и то, что функция-член это по сути обычная функция с неявным первым аргументом this, то такое преобразование вполне разумно и сознательно его ломать создатели компиляторов не станут. Запись в одно поле union и чтение из другого, тоже не соответствует стандарту…
                      С тем что данное преобразование нестандартный хак я в курсе (правда не понимаю, почему бы его не стандартизировать).
                        +1

                        Ну, технически, если стандарт чего не определяет, то это именно что неопределённое поведение.


                        Но зачем плодить не соответствующий стандарту код с хаками, когда есть стандартные подходы (кроме того, обладающие и другими преимуществами, так как на уровне обёртки единообразно можно менять список аргументов согласно используемой библиотеке, например), непонятно.

                          0
                          > Ну, технически, если стандарт чего не определяет, то это именно что неопределённое поведение.

                          Undefined behavior — это четко определенный в стандарте термин, там же написано, что в случае UB компилятор может генерировать любой код (например выкинуть код, который обращается к неинициализированной локальной переменной, т.к. это UB, а значит можно генерировать любой код). В том же стандарте перечислены случаи, которые являются UB. А вот неопределенное в стандарте поведение совсем не обязательно будет UB (даже если тебе хочется его так назвать). И насколько компилятор может гибко подходить к генерации кода — непонятно. Как я уже писал, между UB, implementation defined и unspecified есть разница.

                          > Но зачем плодить не соответствующий стандарту код с хаками
                          Мой код является хаком, привел я его для того, чтобы показать, что можно и на C++11 написать то, что тебе нужно (насколько я понял, твой вариант подходит только для C++17). Более того, в статье ты утверждал, что это сделать нельзя.
                          > Возможно, в рамках C++14 и можем, но я не придумал, как, а скорее нашёл доказательство, что этого сделать нельзя, но поля этой статьи слишком узки для него.
                            0
                            А вот неопределенное в стандарте поведение совсем не обязательно будет UB

                            Если в стандарте не определено поведение, то для любого сгенерированного для соответствующей конструкции кода соответствующая реализация будет соответствовать стандарту, верно?


                            Более того, в статье ты утверждал, что это сделать нельзя.

                            Соответствующим стандарту способом.

                              0
                              Я возразил против вот этой фразы:
                              > Ну, технически, если стандарт чего не определяет, то это именно что неопределённое поведение.
                              По твоей версии получается, что UB все, что не определено в стандарте. По моей версии UB — только то, что явно перечислено в стандарте как UB.

                              > Соответствующим стандарту способом.

                              Это ты только что придумал. Изначально такого условия не было. Очень удобно придумывать условия задним умом и доказывать свою правоту.

                              P.S. Я не призываю нарушать стандарт на право и налево, если что.
                                +1
                                По твоей версии получается, что UB все, что не определено в стандарте.

                                Да, именно так. Если есть мнение, что не определено то, что должно быть определено, то это дефект, можно писать им багрепорт.


                                Это ты только что придумал. Изначально такого условия не было.

                                Изначальное условие — в рамках данного конкретного языка. Стандарт описывает множество допустимых конструкций в языке, значит, надо пользоваться ими.

                      0
                      Ну и я почти уверен, что подобный каст — UB

                      где-то в серии про fast delegate/very fast delegate и так далее до impossible fast delegate этот момент подробно рассматривался. Стоит поглядеть, если интересно.

                        0

                        У меня уже пару месяцев вкладка с этой серией открыта, пора бы и почитать, да.

                    0
                    Да, но в реальных либах никакой гарантии что колбек-функция будет вызываться с первым параметром-указателем на ваш объект (fn( userdata, 7831505 )) нет.
                    Иногда userdata передается последней в колбек (libev), или же userdata вообще не передается (libh2o).
                    Тут, насколько мне известно, только ручные промежуточные функции писать придется.

                    И здесь вопрос — а какой самый быстрый способ? std::bind\std::function весьма дорогие, там генерится куча лишнего кода. Есть ли способы полегче?
                      0
                      Да, но в реальных либах никакой гарантии что колбек-функция будет вызываться с первым параметром-указателем

                      Я не могу вспомнить ни одной библиоетки, где userdata был бы первым параметром, поэтому в предлагемом решении он идёт последним, да и ничего это принципиально не меняет, на самом деле.


                      или же userdata вообще не передается (libh2o)

                      В общем случае таким библиотекам надо писать багрепорты. Конкретно по h2o — тыкнул в пару хедеров случайным образом, а там нет никаких коллбеков.


                      Да и если this никак не передать, то и вспомогательные функции не помогут.


                      И здесь вопрос — а какой самый быстрый способ?</blockquote.
                      Для чего именно?

                      std::bind\std::function весьма дорогие, там генерится куча лишнего кода.

                      Ну, результат std::bind или std::function вы в сишную функцию и не передадите. А код там не лишний, он нужен для type erasure — тот же std::function подразумевает, что вы можете создать объект, содержащий, скажем, лямбду, а потом присвоить ему указатель на функцию и прочие подобные вещи.
                        0
                        >>В общем случае таким библиотекам надо писать багрепорты. Конкретно по h2o — тыкнул в пару хедеров случайным образом, а там нет никаких коллбеков.
                        https://github.com/h2o/h2o/blob/master/examples/libh2o/simple.c#L38 — вот пример установки колбека на получение запроса от http-сервера. Там нет никаких userdata (int (*on_req)(h2o_handler_t *, h2o_req_t *)).
                        То же самое в libuv — см. uv_timer_init.
                        Все они возвращают в колбек указатель на свою структуру, и если вы знаете ее адрес в памяти, то всегда можете получить свои данные извратами типа:
                        #define STRUCT_FROM_MEMBER(s, m, p) ((s *)((char *)(p)-offsetof(s, m)))
                        


                        Вот как приходится работать с libuv из cpp-кода:
                        void Trans::TimeoutHandlerCbGate(uv_timer_t *h) {
                            struct TimeoutHandler *to_h = STRUCT_FROM_MEMBER(struct TimeoutHandler, watcher, h);
                            return to_h->cb(h);
                        }
                        

                        где cb — инициализируется чем-то вроде:
                        to_handler_.cb = std::bind(&Trans::TimeoutHandlerCb, this, std::placeholders::_1); 
                        

                        Если на все это посмотреть в отладчике, то там куча кода генерится лишнего…

                        Как бы вы это реализовали?

                          0
                          вот пример установки колбека на получение запроса от http-сервера. Там нет никаких userdata (int (*on_req)(h2o_handler_t *, h2o_req_t *)).

                          А, я просто другую h2o нашёл:


                          % eix -c libh2o
                          [N] sci-libs/libh2o ((~)0.2.1): Library of routines for IF97 water & steam properties
                          [N] sci-libs/libh2oxx ((~)0.2): C++ bindings for libh2o
                          

                          В любом случае, это плохо задизайненый API. Зачем так делать?


                          Все они возвращают в колбек указатель на свою структуру, и если вы знаете ее адрес в памяти, то всегда можете получить свои данные извратами типа

                          Не вижу, опять же, принципиальной разницы между void *udata и CbData *data с членом данным, соответствующим userdata. Да, это придётся писать руками (ну а как ещё?), но один раз для каждой подобной библиотеки, а не каждый раз в каждой точке установки коллбека.


                          Если на все это посмотреть в отладчике, то там куча кода генерится лишнего…

                          Если cb — это какой-нибудь std::function, то неудивительно.


                          Как бы вы это реализовали?

                          Я сходу не вижу принципиальных препятствий против использования подхода, предложенного в исходном посте, просто вместо прямого каста udata к типу класса придётся чуть больше извращаться, ну вот как у вас с STRUCT_FROM_MEMBER.

                            0
                            Можете помочь, как, например, навесить callback libuv таймера (http://docs.libuv.org/en/v1.x/timer.html) на функцию класса без std::function?
                              0

                              Киньте куда-нибудь минимальный компилируемый пример, попробую глянуть.

                                0
                                Да я в принципе разобрался уже, просто почему-то считал что тут предлагается некий универсальный шаблон на все случаи жизни (не удивился бы), а тут просто оббертка под конкретную либу (вообще тобой же и написанную в итоге).
                                  0

                                  Нет, почему ж тобой написанную. Это подойдёт для любой либы с соответствующим паттерном передачи параметров — список из предпоследнего абзаца, например, из моего опыта.


                                  Для других паттернов нужно эту обёртку подтьюнить, да, но это делается довольно легко. А на все случаи жизни никак, заранее ведь неизвестно, как библиотека это дело передаёт. Можно, конечно, попробовать расширить, указав место, на котором идёт userdata, и (мета)функцию для получения void* из этого userdata, но это уже следующий шаг.

                          0
                          Да и если this никак не передать, то и вспомогательные функции не помогут.

                          К несчастью такие API бывают. К ещё большему несчастью, когда они появляются во всяких SDK, часть которых может быть закрыта. Каждый раз приходится как-то выкручиваться и всякий раз остаётся ощущение неприятного привкуса.


                          Бывает когда дата не передаётся, но передаётся указатель на какой-то объект (структуру), который ты сам же и создавал. Тут помогает обёртка в POD структуру и offsetof()… для вычисления сохраённого указателя на this (такой подход использовал в колбеках DMA в Cypress FX3 SDK /доступна для скачивания апосля регистрации или находится на gtithub/).


                          Короче, с колбеками в C API всякий раз какой-то сюрприз обнаруживается.

                        0

                        Почему для это не использовать std::bind? Что-то вроде:


                        auto func = std::bind(&A::someFunc, a, std::placeholders::_1);

                        func — можем передавать дальше куда душе угодно

                          +1

                          Потому что возвращаемое значение std::bind не преобразуется в указатель на сишную функцию.

                          +1
                          Но это всё выглядит страшновато и плохо пахнет. Можем ли мы лучше?

                          Возможно, в рамках C++14 и можем, но я не придумал, как, а скорее нашёл доказательство, что этого сделать нельзя, но поля этой статьи слишком узки для него.


                          Можем, если вынести враппер на уровень выше:
                          call_c_func( doWithCallback, foo ).with_callback<&Foo::doFoo>();
                          

                          Код
                          #include <tuple>
                          
                          namespace detail {
                          
                          template <class T, class = std::tuple<>>
                          struct pop_back;
                          
                          template <class... Result>
                          struct pop_back<std::tuple<void*>, std::tuple<Result...>> {
                              using type = std::tuple<Result...>;
                          };
                          
                          template <class First, class Second, class... Tail, class... Result>
                          struct pop_back<std::tuple<First, Second, Tail...>, std::tuple<Result...>>
                            : pop_back<std::tuple<Second, Tail...>, std::tuple<Result..., First>> {
                          };
                          
                          template <class F>
                          struct c_func_info;
                          
                          template <class R, class... Args>
                          struct c_func_info<R ( * )( Args... )> {
                              using result_type = R;
                              using args        = std::tuple<Args...>;
                          };
                          
                          template <class R, class... Args>
                          struct c_func_info<R ( & )( Args... )> : c_func_info<R ( * )( Args... )> {
                          };
                          
                          } // namespace detail
                          
                          template <class, class, class, class>
                          struct Wrapper;
                          
                          template <class F, class C, class R, class... Args>
                          struct Wrapper<F, C, R, std::tuple<Args...>> {
                          
                              template <R ( C::*Ptr )( Args... ), class... Args2>
                              R with_callback( Args2... args2 )
                              {
                                  return f( args2..., []( Args... args, void* userdata ) { return ( static_cast<C*>( userdata )->*Ptr )( args... ); }, c );
                              }
                          
                              explicit Wrapper( F f_, C* c_ )
                                : f( f_ )
                                , c( c_ )
                              {
                              }
                          
                              F f;
                              C* c;
                          };
                          
                          template <class F, class C>
                          auto call_c_func( F f, C& c )
                          {
                              using func_info     = detail::c_func_info<F>;
                              using callback_info = detail::c_func_info<typename std::tuple_element<std::tuple_size<typename func_info::args>::value - 2,
                                                                                                    typename detail::pop_back<typename func_info::args>::type>::type>;
                              using callback_args = typename detail::pop_back<typename callback_info::args>::type;
                              return Wrapper<F, C, typename callback_info::result_type, callback_args>{f, &c};
                          }
                          
                          //
                          
                          #include <iostream>
                          
                          int doWithCallback( int value, int ( *fn )( int, void* ), void* userdata )
                          {
                              return fn( value, userdata ) * 3;
                          }
                          
                          struct Foo {
                              explicit Foo( int i )
                                : m_i( i )
                              {
                              }
                          
                              int m_i;
                          
                              int doFoo( int val )
                              {
                                  std::cout << m_i << " vs " << val << std::endl;
                                  return m_i * 2;
                              }
                          };
                          
                          
                          int main()
                          {
                              Foo foo{42};
                              auto v = call_c_func( doWithCallback, foo ).with_callback<&Foo::doFoo>( 10 );
                              std::cout << v << std::endl;
                          }
                          

                            0

                            Изящное решение, спасибо!

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

                          Самое читаемое