I am trying to figure out how to "overload" a variadic function template with a "more specialized" variadic function template. For example:
#include <iostream>
template <typename... ARGS_>
void foo(void(*fn)(ARGS_...)) {
std::cout << "Generic function pointer foo" << std::endl;
}
struct Test {
};
template <typename... ARGS_>
void foo(void(*fn)(ARGS_..., Test*)) {
std::cout << "Test function pointer foo" << std::endl;
}
void test1(int a, int b) {
std::cout << "test1()" << std::endl;
}
void test2(int a, int b, Test* x) {
std::cout << "test2()" << std::endl;
}
int main() {
foo(&test1);
foo(&test2);
return 0;
}
The output of this code is:
Generic function pointer foo
Generic function pointer foo
Rather than:
Generic function pointer foo
Test function pointer foo
as I want.
Conceptually, I am trying to notate "Use template method A if you have any type(s) of arguments where the LAST on is Test* and use template method B if the last type is NOT Test*."
What is the correct method to accomplish this type of behavior?
You may overload a function template either by a non-template function or by another function template. The function call f(1, 2) could match the argument types of both the template function and the non-template function.
Variadic templates are class or function templates, that can take any variable(zero or more) number of arguments. In C++, templates can have a fixed number of parameters only that have to be specified at the time of declaration. However, variadic templates help to overcome this issue.
This means that all functions that have 1 or more arguments are matched to the variadic template and all functions that with no argument are matched to the empty function. This article is contributed by MAZHAR IMAM KHAN.
………. …… ….. ……. where, T is template argument accepting different arguments and class is a keyword. The name of the function templates are the same but called with different arguments is known as function template overloading.
A template function can be overloaded either by a non-template function or using an ordinary function template. Function Overloading: In function overloading, the function may have the same definition, but with different arguments. Below is the C++ program to illustrate function overloading:
When using variadic templates you may end up in a situation, where you would like to know how many arguments are actually passed. Let’s say that you want to store them in a table. How big should it be? sizeof... () will tell you: template <typename... Args> void func (Args... args) { int argsTable [sizeof... (args)] = {args...};
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
}
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.
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.
Check that the last element of ARGS...
is Test*
.
It won't do that for you this way. One ways is:
template<class...Ts>
struct last_type {};
template<class T1, class T2, class...Ts>
struct last_type<T1, T2, Ts...>:last_type<T2, Ts...>{};
template<class T>
struct last_type<T>{
using type=T;
};
template<class...Ts>
using last_type_t = typename last_type<Ts...>::type;
now you just:
template <typename... ARGS_>
requires std::is_same_v<last_type_t<ARGS_...>, Test*>
void foo(void(*fn)(ARGS_...)) {
std::cout << "Test function pointer foo" << std::endl;
}
Live example.
Without concepts, you have to replace that requires
clause:
template <typename... ARGS_,
std::enable_if_t<std::is_same_v<last_type_t<ARGS_...>, Test*>, bool> = true
>
void foo(void(*fn)(ARGS_...)) {
std::cout << "Test function pointer foo" << std::endl;
}
which is a more obscure "cargo cult" way to basically say the same thing. (you also need to invert enable if clause in the other overload; not = false but !, and handle 0 arg case (prepend void on the type list?))
The reason why your attempt doesn't work is that C++ makes ...
matching insanely greedy. It is generally not a good idea to put things you want pattern matched behind it in a context where pattern matching of parameters is done.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With