SFINAE'd overloads based on the last parameter pack argument
You can add mutually exclusive overloads based on whether the last type in the variadiac parameter pack is Test* or not:
#include <type_traits>
template <typename... Ts>
using last_t = typename decltype((std::type_identity<Ts>{}, ...))::type;
struct Test {};
template <
typename... ARGS_,
std::enable_if_t<!std::is_same_v<last_t<ARGS_...>, Test *>> * = nullptr>
void foo(void (*fn)(ARGS_...)) {
std::cout << "Generic function pointer foo" << std::endl;
}
template <
typename... ARGS_,
std::enable_if_t<std::is_same_v<last_t<ARGS_...>, Test *>> * = nullptr>
void foo(void (*fn)(ARGS_...)) {
std::cout << "Test function pointer foo" << std::endl;
}
// Special case for empty pack (as last_t<> is ill-formed)
void foo(void (*fn)()) { std::cout << "Zero args" << std::endl; }
making use if C++20's std::type_identity for the last_t transformation trait.
Used as:
void test1(int, int b) {}
void test2(int, int b, Test *x) {}
void test3(Test *) {}
void test4() {}
int main() {
foo(&test1); // Generic function pointer foo
foo(&test2); // Test function pointer foo
foo(&test3); // Test function pointer foo
foo(&test4); // Zero args
}
Avoiding the zero-arg special case as an overload?
The zero-arg foo overload can be avoided in favour of tweaking the last_t trait into one which also accepts an empty pack, such that a query over the empty pack is used to resolve to the generic overload. Neither its semantics nor its implementation becomes as straight-forward and elegant, however, as "the last type in an empty type list" does not make much sense, meaning the trait need to be tweaked into something different:
template <typename... Ts> struct last_or_unique_dummy_type {
using type = typename decltype((std::type_identity<Ts>{}, ...))::type;
};
template <> class last_or_unique_dummy_type<> {
struct dummy {};
public:
using type = dummy;
};
template <typename... Ts>
using last_or_unique_dummy_type_t =
typename last_or_unique_dummy_type<Ts...>::type;
template <typename... ARGS_,
std::enable_if_t<!std::is_same_v<
last_or_unique_dummy_type_t<ARGS_...>, Test *>> * = nullptr>
void foo(void (*fn)(ARGS_...)) {
std::cout << "Generic function pointer foo" << std::endl;
}
template <typename... ARGS_,
std::enable_if_t<std::is_same_v<last_or_unique_dummy_type_t<ARGS_...>,
Test *>> * = nullptr>
void foo(void (*fn)(ARGS_...)) {
std::cout << "Test function pointer foo" << std::endl;
}
Using an additional overload for the empty pack is likely the least surprising approach.
C++20 and the identity_t trick
In case you are not yet at C++20, an identity meta-function is trivial to write yourself:
template <typename T>
struct type_identity {
using type = T;
};
Any specialization of this class template, unless partially/explicitly specialized otherwise (which is UB for the STL type), is trivial and default-constructible. We leverage this in the definition of last_t above: default-constructing a series of trivial types in an unevaluated context, and leveraging that the last of those types embeds the input to the identity trait whose specialization is that trivial type, and whose wrapped alias declaration type is the type of the last parameter in the variadic parameter pack.