(How) Can I infer a template type argument at one place and “just use” the inferred type elsewhere?

120 Views Asked by At

My function template has (among others) a function pointer (or a member function pointer) parameter. The function pointer may take any arguments, the argument types are being inferred. At the end of the parameter list, I want the function template to take the same parameters as the function pointer parameter.

template<typename R, typename... Args>
void f(R(*fp)(Args...), Args... args);

The problems begin when the argument types do not match perfectly, e.g. let’s say fp takes a Base* and the caller to f gives you a Derived* or fp takes a double and the caller puts in 0 (of type int).

Is there any way to tell the compiler to infer Args solely based on fp and “just use” those inferred types for args and not try to match them based on the arguments they bind to?

2

There are 2 best solutions below

0
Bolpat On

I found a solution that works. The term I did not know is non-deduced context. For example, everything to the left of the scope resolution operator :: is non-deduced. In C++20, you can use std::type_identity_t to create such a context artificially, in C++14 and C++17, you can make your own type_identity_t.

So, you’d use

template<typename R, typename... Args>
void f(R(*fp)(Args...), std::type_identity_t<Args>... args);

About perfect forwarding, when fp takes an argument by reference or value, f will do the same. With std::forward, a by-value parameter will be moved and a reference parameter will be referenced.

0
Holt On

One option is to deduce different types for both1 and use a SFINAE or concepts to disable the overload when:

// no SFINAE
template <typename R, typename... Args, typename... UArgs>
void f(R (*fp)(Args...), UArgs&&... args);

// SFINAE
template <typename R, typename... Args, typename... UArgs>
auto f(R (*fp)(Args...), UArgs&&... args)
    -> std::void_t<std::invoke_result_t<decltype(fp), UArgs...>>;

// concepts
template <typename R, typename... Args, typename... UArgs>
auto f(R (*fp)(Args...), UArgs&&... args)
    requires std::invocable<decltype(fp), UArgs...>;

One difference between this option and the std::type_identity one is when you call f with arguments that need a conversion for fp, e.g.

struct X { X(int); };
void f1(X);

f(&f1, 33); // implicit conversion from int to X

In the std::type_identity case, the conversion is done on the caller side (i.e., when you call f), while with the option above, the conversion is done when you invoke fp inside f.


1 You could also use a generic function type, like proposed in the comments, depending on what you do inside f().