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

в 3:52, , рубрики: c++, C++14, c++17

Энное время назад в одной 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);
}

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

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

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

Автор: 0xd34df00d

Источник

Поделиться

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