📜 ⬆️ ⬇️

Pass pointers to member functions in the C API

Enno time ago in one XMPP-room dedicated to C ++, one visitor asked if there was any way in modern pluses without extra code to pass a pointer to a class member function as a callback to the C API. Well, something like:

// 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); } 

It is clear that as a MAGIC you can use a free function, a static member function or even a lambda (2017 is the year outside, after all), but writing the appropriate construction each time for each function with your hands is somewhat lazy, and the preprocessor, as we all Of course, we know - moveton.

In this post we will try (and, characteristically, we will succeed) to write a universal wrapper, and at the same time we will see how some C ++ 17 chip will help us to further reduce the amount of redundant code. There will be no cool templates here, the solution, in my opinion, is rather trivial, but perhaps it still makes sense to share them (and at the same time once again promote new C ++ 17 features).

First, we solve the problem more simply: suppose that the member function does not take any arguments (except for the implicit this , of course, which we have to pass). A typical frontal solution would then look something like this:
')
 doWithCallback ([] (void *udata) { return static_cast<Foo*> (udata)->doFoo (); }, &foo); 

Draft code from which we will build
 #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); } 


If we want to make a generalized wrapper, then, of course, it must somehow be passed at the point of creation of the callback, which function over which particular class we want to call. In addition, the wrapper must be saved, in fact, what exactly it needs to call. If everything is simple with the transfer (we took a pointer to a member function and passed it), then saving it will not be so easy:

code like:

 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); } 

it is expected not to be collected, since lambdas with a non-empty capture list cannot be converted to a function pointer:

 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) ^ 

It makes sense: there is not enough “space” in the function pointer to save, in addition to the function itself, to save some context (or at least a pointer to it).

So what to do? We are doomed?

Not! Non-type parameters come to the rescue: in the overwhelming majority of cases, when transferring a callback, we know at the compilation stage what function we want to call, which means we can parameterize some template with this, and we don’t have to carry any information in runtime.

Just as we can parameterize a template with, say, an integer, we can parameterize it with a function pointer. Let's try:

 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); } 

Works! But there is one problem: the type of the return value, the type of the class, and the list of the types of the parameters of the function (in our case it is empty) must be specified each time by hand. But laziness is the same. Can we do better?

In C ++ 11/14, we can force the compiler to infer the above types, but for this we have to specify the desired member function twice: once to infer the type of the variable corresponding to the pointer to this function, from which we can already get everything we need to form the correct “signature” for the non-type argument of the template. Something like this:

 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); } 


Full code
 #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); } 



But it all looks scary and smells bad. Can we do better?

Perhaps in the framework of C ++ 14 we can, but I did not think of how, but rather I found evidence that this cannot be done, but the fields of this article are too narrow for it.

So, the main problem is that we have to explicitly specify the type of the template non-type argument, for which we need to explicitly specify all these types of return values ​​and other similar things in one form or another. Fortunately, in C ++ 17 we added exactly what we needed: automatic output of the type of the template argument (it works so far only in the developed clang and gcc branches). The required code is significantly simplified:

 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); } 

Everything.

Oh, no, not all. Recall that initially our member function could accept an arbitrary set of parameters. Similarly, DetectArgsImpl , which returns a list of argument types, can be defined:

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

and use partial specialization to deploy it:

 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))> {} (); } 

Together
 #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); } 


So it goes. You can safely take the Tox API, the libpurple API, the gstreamer API, any kind of API and avoid the heap of boilerplate.

As an exercise, an interested reader can add an indication of the arguments that are passed to the callback sishny API, but which should be ignored - for example, Tox first argument passes a pointer to the same, which may well be available in our plus code.

And the habrahrabrovsky syntax coloring from all this is bad, it seems.

Source: https://habr.com/ru/post/321934/


All Articles