Search code examples
c++templatesfunction-pointerstype-deduction

Deduction of Return / Argument types from function


I've got a hierarchy of types for Callback objects. After some heavy simplification, it looks a bit like this:

template <typename Return, typename... Args>
struct Callback
{
  virtual Return Call(Args...) =0;

  void* mCtx;
}; // ctor, virtual dtor omitted

template <class T, typename Return, typename... Args>
struct MemberCallback: Callback<Return, Args...>
{
  Return Call(Args... args) override
  {
    return (static_cast<T*>(mCtx)->*mFn)(args...);
  }
  
  Return(T::*mFn)(Args...);
}; // ctor, specialisation for Return = void omitted

template <typename Return, typename... Args>
struct FunctionPtrCallback: Callback<Return, Args...>
{
  Return Call(Args... args) override
  {
    return mFn(args..., mCtx);
  }
  
  Return(*mFn)(Args..., void*);
}; // ctor, specialisation for Return = void omitted

And a factory function to deduce the type of MemberCallback<>s:

template <class T, typename Return, typename... Args>
MemberCallback<T, Return, Args...> MakeCallback(T& object, Return(T::*fn)(Args...))
{
  return MemberCallback<T, Return, Args...>(&object, fn);
} // overload for const methods omitted

I'm trying to set up a similar factory function for FunctionPtrCallback<>s, however I keep running into errors:

The naive approach, FunctionPtrCallback<Return, Args...> MakeCallback(Return(*fn)(Args..., void*), void*) yields complaints about failing to find a suitable overload (C2672) after failing to deduce template argument for Return (__cdecl *)(Args...,void *) from void (ActualTypeUsed,void *) (C2784).

EDITs: specifying the template arguments explicitly - MakeCallback<void, ActualTypeUsed> will get it to compile, except for functions that only take a single void* (MakeCallback<void>()).

(If I omit the void* argument from the signature of fn in the MakeCallback declaration, it compiles, but instantiates the wrong FunctionPtrCallback template - FunctionPtrCallback<void, ActualTypeUsed, void*> instead of FunctionPtrCallback<void, ActualTypeUsed>.)

I have then tried to use an interim 'function traits' template, akin to the one suggested in https://stackoverflow.com/a/9065203/79344 :

template <typename T>
struct FnTraits;

template <typename Return, typename... Args>
struct FnTraits<Return(Args...)>
{
  FunctionPtrCallback<Return, Args...> MakeCallback(Return(*fn)(Args..., void*), void* data)
  {
    return FunctionPtrCallback<Return, Args...>(fn, data);
  }
};

The complaint is now about using an incomplete type in nested name specifier for my FnTraits<decltype(MyCallback)>::MakeCallback(MyCallback, myData). Placing the declaration of MyCallback before FnTraits<> (however impractical), doesn't help either.

1, In neat, academical terms, why is my approach wrong / what am I misunderstanding about template deduction?

2, If there is a way to make it work, getting Return and Args... from my callbacks auto-deduced, what is it?

(I'm aware of std::function<>; I'd prefer to avoid using it.)

EDIT: godbolt link


Solution

  • Unfortunately, it seems like a pack expansion where the pack is not at the end of the list, such as Return(*)(Args..., void*), is a non-deduced context (see #8 here; disclaimer: I'm not a language lawyer). This means that Args cannot be deduced from the type of FooBaz in your example.

    However, you can work around this by making it so the parameter pack is deduced to include the void*, and then removing the void* later: https://godbolt.org/z/aTTqbc

    // Given Args that doesn't include void*, take the parameter pack
    // out of the tuple and put it into the desired FunctionPtrCallback type.
    template <typename Return, typename Tuple>
    struct MakeCallbackImpl;
    template <typename Return, typename... Args>
    struct MakeCallbackImpl<Return, std::tuple<Args...>> {
        using type = FunctionPtrCallback<Return, Args...>;
    };
    
    // Given Args that *does* include void*, remove it using RemoveLast.
    // Args can now be deduced because it appears last in Return(*)(Args...).
    template <typename Return, typename... Args>
    auto MakeCallback(Return(*fn)(Args...), void* data) {
        return typename MakeCallbackImpl<Return, typename RemoveLast<Args...>::type>::type(fn, data);
    }
    

    And the helper RemoveLast, inspired by this forum thread (but using std::tuple_cat as a convenience instead of defining a custom concat):

    template<typename First, typename... Rest>
    struct RemoveLast {
        using type = decltype(std::tuple_cat(
            std::declval<std::tuple<First>>(),
            std::declval<typename RemoveLast<Rest...>::type>()
        ));
    };
    
    template<typename First>
    struct RemoveLast<First> {
        using type = std::tuple<>;
    };