Deduce argument types from templated callable type

Viewed 97

This questions has been asked before on Stackoverflow, but all answers I found were pre C++ 20 ones that did not exactly give a solution to my problem. I'm hoping that things have evolved.

I have a function which gets some callable type (a lambda, a function pointer, a struct with a call operator) and I need to deduce the argument types of the object passed in to create a tuple of that types. I think of something like this piece of pseudocode

template <class Callable>
void foo (Callable&& callable)
{
    using ArgsTuple = std::tuple<ArgumentTypesOfCallable...>; 
    
    // Do some fancy stuff with that tuple
}

I'm pretty sure that there must be some way of deducing these types with C++ 20, but I can't figure out how.

1 Answers

I don't think you need C++20 to do this, but you do need to separate out the arguments you're planning to call your function with if you want to do anything useful. Here's a simple (full) example that I think will do what you want, though I have no idea what you actually plan to do with the tuple you're making:

#include <iostream>
#include <tuple>
#include <string>
#include <functional>
    
template<typename Func, class... Args>
void DoUniqueCorrectly(Func&& fn, Args&&... args) 
{
  using ArgsTuple = std::tuple<Args...>;

  std::cout << "Getting ready to invoke\n";
  ArgsTuple tup(args...);
  if constexpr (std::tuple_size<ArgsTuple>{} > 0) {
      std::cout << "arg1 = " << std::get<0>(tup);
  }
  if constexpr (std::tuple_size<ArgsTuple>{} > 1) {
      std::cout << ", arg2 = " << std::get<1>(tup);
  }
  std::cout << '\n';
  std::invoke(std::forward<Func>(fn), std::forward<Args>(args)...);
}

void test(const char *arg1, int arg2)
{
    std::cout << "test called! arg1 = " << arg1 << ", arg2 = " << arg2 << '\n';
}

int main(int argc, char **argv)
{
    DoUniqueCorrectly(test, "hey!", 22);
    return 0;
}

Output:

Getting ready to invoke
arg1 = hey!, arg2 = 22
test called! arg1 = hey!, arg2 = 22

Try it yourself on godbolt

The printing of tuple elements is placeholder, to do a proper generic iterator would take up a lot more space. You can find plenty of other questions on stack overflow that do that the "right" way.

Edit: As Ben Voigt points out, the elements of the ArgsTuple are going to have a type which is identical to the types passed to DoUniqueCorrectly, but not necessarily identical to the types of the functional call. A good way to illustrate this is to change the function signature of test() to:

void test(const std::string arg1, int arg2)

Since we invoked DoUniqueCorrectly with a const char *, the ArgsTuple will have type <const char *, int>, and this will be converted to const std::string on the invoke call. This may or may not be important in your particular application, but it's certainly important to be aware of if you adopt a solution like this.

Related