4
\$\begingroup\$

I've started to write a header-only implementation of callback for member functions :


#include <cstddef>
#include <utility>



namespace _callback_internal{

/** \internal
 *  A Type list to store variable argument type
 */
template<typename T, typename... Args>
struct TypeRecList{
  template<size_t n, bool dummy = true> // dummy is needed for the partial specialization
  struct get{
    using target = typename TypeRecList<Args...>::template get<n - 1>;
    using type = typename target::type;
    using tail = typename target::tail;
  };
  
  template<bool dummy>
  struct get<0, dummy>{
    using target = get<0>;
    using type = T;
    using tail = TypeRecList<Args...>;
  };

};

/**
 * Canonical definition of TypedCallbackObject, so that the compiler does not complain about unrelated type
 * when accessed with ::CallbackObject<method>
 */
template <typename C, typename R, typename... Args>
struct TypedCallbackObject{
  C * this_;
  R (*callback)(C *, Args...);
  
  template <typename _C>
  inline constexpr operator TypedCallbackObject<_C, R, Args...>() {
    return TypedCallbackObject<_C, R, Args...>{reinterpret_cast<TypedCallbackObject<_C, R, Args...>>(*this)};
  }
  
  template <typename _C>
  inline constexpr TypedCallbackObject<_C, R, Args...> & castClass() {
    return *reinterpret_cast<TypedCallbackObject<_C, R, Args...>*>(this);
  }

};

/**
 * Same as TypedCallbackObject, but with a void `this_`
 */ 
template <typename R, typename... Args>
using CallbackObject = TypedCallbackObject<void, R, Args...>;

template<typename R, typename C, typename... Args>
struct CallbackDetails;

/**
 * The callback class, holding addresses of the static version of the members generated at compile time.
 */
template<auto f, typename R, typename C, typename... Args>
struct Callback{
  using Self = Callback<f, R, C, Args...>;
  /**
   * Canonical c function version of the member, taking void as first argument. mfunc permits to reinterpret cast to a function taking any type as first arg.
   */
  static R cfunc(void * _this, Args... args){
    return (static_cast<C *>(_this)->*f)(args...);
  }
  /**
   * Allow to change the first argument from a void pointer to anything.
   */
  template<typename T=void>
  static constexpr auto mfunc(){
    return reinterpret_cast<R (*)(T *, Args...)>(Self::cfunc);
  }
  using details_t = CallbackDetails<R, C, Args...>;
  using return_t = typename details_t::return_t;
  using class_t = typename details_t::class_t;
  using args_list_t = TypeRecList<Args...>;
  template <typename T=void>
  using CallbackObject = typename details_t::template CallbackObject<T>;
  
  template<typename T=void>
  static inline constexpr CallbackObject<T> callback(C * this_){
    return CallbackObject<T>{
      this_,
      Self::template mfunc<T>()
    };
  }
};

/**
 * Detail class the extract the information about the callback type.
 */
template<typename R, typename C, typename... Args>
struct CallbackDetails{
  using return_t = R;
  using class_t = C;
  using args_list_t = TypeRecList<Args...>;
  using cfunc_t = R(C::*)(Args...);
  template<auto f>
  using Callback = Callback<f, R, C, Args...>;
  template<typename T=void>
  using CallbackObject = TypedCallbackObject<T, R, Args...>;
};

/** \internal
 * Utility function to be used with `decltype()` to extract the details.
 */
template<typename R, typename C, typename... Args>
CallbackDetails<R, C, Args...> get_details(R (C::*) (Args...));

}

/**
 * Callback object class can use to group the function pointer and the first argument (bind-like)
 */
template <typename C, typename R, typename... Args>
using TypedCallbackObject = _callback_internal::TypedCallbackObject<C, R, Args...>;

/**
 * Same as TypedCallbackObject with void * as first function argument.
 */
template <typename R, typename... Args>
using CallbackObject = _callback_internal::CallbackObject<R, Args...>;

/**
 * Return the internal Callback object providing all informations abour the method.
 */
template <auto method>
using CallbackTraits = typename decltype(_callback_internal::get_details(method))::template Callback<method>;

/**
 * Construct a c function pointer that will have the same signature as \p method, but with a `void *` argument (or \p T if provided), wich should be an instance of the class method is bound to.
 */
template <auto method, typename T=void>
inline constexpr auto toCallback(){ return CallbackTraits<method>::template mfunc<T>(); }


/**
 * Construct a callback object wich binds an instance to a c function pointer that will call method from the instance.
 */
template <auto method, typename T=void>
inline constexpr auto toCallbackObject(typename CallbackTraits<method>::class_t * instance){ return CallbackTraits<method>::template callback<T>(instance); }

Here a main.cpp to test it :

#include "./callback.h"
#include <iostream>
#include <sstream>
#include <string>

void f(void * instance, std::string (*g)(void *, float, int)){
  std::cout<<"g addr : <"<<(void *)g<<"> result:"<<g(instance, 1.5, 9)<<std::endl;
}

struct A{
  int coef;
  public:
  A(int coef):coef{coef} {}

  std::string add(float f, int i){
    std::ostringstream oss;
    oss << "Add function : " << coef << "×("<< f << "+" << i << ") ) " << coef*(f+i);
    return oss.str();
  }
  
  std::string mult(float f, int i){
    std::ostringstream oss;
    oss << "Add function : " << coef << "×("<< f << "*" << i << ") ) " << coef*(f*i);
    return oss.str();
  }
};

int main(int argc, char * argv[]){
  
  A a1{1};
  A a2{2};

  f(&a1, toCallback<&A::add>());
  f(&a2, toCallback<&A::add>());
  f(&a1, toCallback<&A::mult>());
  f(&a2, toCallback<&A::mult>());

  CallbackObject<std::string, float, int> oba1 = toCallbackObject<&A::add>(&a1);
  f(oba1.this_, oba1.callback);
  CallbackObject<std::string, float, int> oba2 = toCallbackObject<&A::add>(&a2);
  f(oba2.this_, oba2.callback);
  
  TypedCallbackObject<A, std::string, float, int> oba2_ = toCallbackObject<&A::add, A>(&a2);
  f(oba2_.castClass<void>().this_, oba2_.castClass<void>().callback);

  return 0;
}

I tried to use the least of C++17 features possible, but I don't see how I could get rid of the auto in the templates...

Do you think removing them is worth it, or should I consider that most compilers are c++17 compatible ? (Note that I would like to target an embedded system... but from what I see, these compilers are mostly based on GCC or Clang...)

Review appreciated too :)

\$\endgroup\$
15
  • 1
    \$\begingroup\$ Anyway what you are trying to do can simply be done like this. Unless I am missing something. Let me know and dont fear auto. It is awesome. godbolt.org/z/h13jhYoaY \$\endgroup\$ Dec 14, 2023 at 14:52
  • \$\begingroup\$ If I'm not mistaking, there are some platform that uses a different call convention for free functions and member calls. see : softwareengineering.stackexchange.com/questions/376746/… With my solution, the only "reinterpret_cast" occurs on the this_ pointer type (It's more likely that all pointer types have the same size). Then, the call to the member is performed using c++ convention. The only other assumption is that static function are called like c functions \$\endgroup\$
    – hl037_
    Dec 14, 2023 at 15:26
  • \$\begingroup\$ Yes, my code only works because of gcc implementation. But that is true for your soluuib too too. No C++ function is automatically a C function, you have to give it one by using extern "C". You cannot do that for templates. \$\endgroup\$ Dec 14, 2023 at 19:01
  • \$\begingroup\$ Sorry, I first though you returned the pointer to member reinterpreted to a free function. But I misread your code : you can actually use the same technique as I did, using template to avoid duplication of the pointer. My code just adds the syntactic sugar avoiding to pass the arguments to the template. (plus some utility types) \$\endgroup\$
    – hl037_
    Dec 14, 2023 at 19:59
  • 2
    \$\begingroup\$ 1) C++17 is not only well-supported, it is the default mode in most compilers these days. In fact, creeping toward minimal C++20 support (so far concepts, coroutines, and ranges are okay, modules not so much) is more the norm. 2) The calling conventions are not only different between static and member functions, they are different between C and C++ functions (for example, with name mangling). So this technique will not actually work. \$\endgroup\$
    – indi
    Dec 14, 2023 at 23:08

1 Answer 1

1
\$\begingroup\$

Don't do this:

template <typename _C>

Such names are reserved in C++:

Each identifier that contains a double underscore __ or begins with an underscore followed by an uppercase letter is reserved to the implementation for any use.

That phrase "for any use" means that _C might be a macro that expands to something unexpected here.


Generally, the code looks over-complex for a simple wrapper around std::mem_fun that passes the object argument as a void*.


If you intend to call from a C program, the function pointer should be declared with extern "C" linkage, as the binary interface may differ between C and C++ calling conventions.


The code assumes that std::size_t is also defined in the global namespace, but that's not necessarily the case. Best to refer to it by its full name, rather than add another portability risk.

\$\endgroup\$
3
  • \$\begingroup\$ std::mem_fun() is deprecated in C++17, you should use std::mem_fn() instead. Is it really that simple to write a wrapper for it though that does what OP wants? \$\endgroup\$
    – G. Sliepen
    Dec 18, 2023 at 16:39
  • \$\begingroup\$ Thanks - I hadn't even spotted that those two are different things, due to the very similar names! \$\endgroup\$ Dec 18, 2023 at 16:42
  • 1
    \$\begingroup\$ I haven't actually tried to implement it myself, so my perception that it looks complex might be off. That said, looking at how std::mem_fn works might be instructive. \$\endgroup\$ Dec 18, 2023 at 16:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.