I'm trying to use recursion with variadic templates. I would like the base case to have zero template arguments. After looking through stackoverflow answers to previous questions, I have found two kinds of responses to this problem:
template <typename = void>
or template <typename T = void>
. For example, the first answer here: How to write a variadic template recursive function?
I attempted to use the solution (2) in my problem, but received errors. This is a Minimal, Reproducible Example:
#include <iostream>
template<typename = void> // base case
int NumArguments() {
return 0;
}
template<typename FirstArg, typename... RemainingArgs>
int NumArguments() {
return 1 + NumArguments<RemainingArgs...>();
}
class A {
public:
A() {}
};
int main() {
std::cout << NumArguments<A>();
return 0;
}
Compilation in Microsoft Visual C++20 gave the errors:
error C2668: 'NumArguments': ambiguous call to overloaded function
message : could be 'int NumArguments<A,>(void)'
message : or 'int NumArguments<A>(void)'
message : while trying to match the argument list '()'
What does this error message mean? How do I create a zero-argument base case for recursion with variadic templates?
Edit: There were requests in the comments for a more complete description of my problem. The question really is the question title, and not "how do I get my code to work?", but I have not yet gotten my code to compile, so I have decided to share it.
NumArguments
is a stand-in for another function ComputeSize
that takes as input Args
and returns an std::size_t
.
template<typename = void>
constexpr std::size_t ComputeSize() {
return 0;
}
template<typename FirstArg, typename... RemainingArgs>
constexpr std::size_t ComputeSize() {
return FuncReturnSize<FirstArg>() + ComputeSize<RemainingArgs...>();
}
The possible list of Arg
s in Args
is finite and known prior to compilation. FuncReturnSize
is overloaded for each of these Args
. For example, two possible "overloads"(?) are
template <typename T>
requires ((requires (T t) { { t.Func()} -> std::same_as<double>; }) || (requires (T t) { { t.Func() } -> std::same_as<std::vector<double>>; }))
constexpr std::size_t FuncReturnSize() {
return 1;
}
template <typename T>
requires requires (T t) { { t.Func() } -> is_std_array_concept<>; }
constexpr std::size_t FuncReturnSize() {
return std::tuple_size_v<decltype(std::declval<T&>().Func())>;
}
The concept is_std_array_concept<>
should check if the return value of t.Func()
is some size array. I am not yet sure if it works. It is defined by
template<class T>
struct is_std_array : std::false_type {};
template<class T, std::size_t N>
struct is_std_array<std::array<T, N>> : std::true_type {};
template<class T>
struct is_std_array<T const> : is_std_array<T> {};
template<class T>
struct is_std_array<T volatile> : is_std_array<T> {};
template<class T>
struct is_std_array<T volatile const> : is_std_array<T> {};
template<typename T>
concept is_std_array_concept = is_std_array<T>::value;
I want all of this computation to be done at compile-time, so I have defined
template<std::size_t N>
std::size_t CompilerCompute() {
return N;
}
I should now be able to ComputeSize
at compile time like so:
CompilerCompute<ComputeSize<Args...>()>()
The error message means exactly what it says, the call is ambiguous.
template<typename = void> // base case
constexpr int NumArguments() {
return 0;
}
This is not a template function that takes 0 arguments, this is a template function that takes one argument that's defaulted (so if the argument isn't specified, it's void). This means that NumArguments<A>()
is a perfectly valid call to this function.
But, NumArguments<A>()
is also a perfectly valid call to the variadic overload with an empty variadic pack (the NumArguments<A,>()
overload listed in the error message).
What sets your case apart from the linked example is that in the linked example, the variadiac overload is templated on int
s, not on types, so there's no ambiguity there. I've copied that implementation here:
template<class none = void>
constexpr int f()
{
return 0;
}
template<int First, int... Rest>
constexpr int f()
{
return First + f<Rest...>();
}
int main()
{
f<1, 2, 3>();
return 0;
}
Notice, the second overload of f
is a variadic template where each template parameter must be an int
value. Calling f<A>()
won't match that overload if A is a type, so the ambiguity is avoided.
It's not possible to declare a zero-argument template function, so you're out of luck there. However, you can instead convert this to a class template as class templates can be partially specialized.
template <class ...Args>
struct NumArguments;
template <>
struct NumArguments<> {
static constexpr int value = 0;
};
template <class T, class ...Args>
struct NumArguments<T, Args...> {
static constexpr int value = 1 + NumArguments<Args...>::value;
};
This specific implementation could, of course, by simplified to use sizeof...
, but the OP has indicated that their real use case is more complicated.
Here's another solution (without specialization), which uses a C++20 requires
clause to resolve the ambiguity:
template <typename... Args> requires (sizeof...(Args) == 0)
constexpr int NumArguments() {
return 0;
}
template<typename FirstArg, typename... RemainingArgs>
constexpr int NumArguments() {
return 1 + NumArguments<RemainingArgs...>();
}
Example:
int main() {
std::cout << NumArguments<int>() << std::endl;
std::cout << NumArguments() << std::endl;
std::cout << NumArguments<float, int, double, char>() << std::endl;
return 0;
}
1
0
4
EDIT:
My old suggestion using concepts
was incorrect. There's a good post here on using concepts and parameter packs.
You should guarantee the end of your variadic template without overloading the function.
A solution compiling with c++ standard 17 (in Microsoft Visual /std:c++17) is the following:
#include <iostream>
//Remove or comment base case!
template<typename FirstArg=void, typename... RemainingArgs>
constexpr int NumArguments() {
if (sizeof...(RemainingArgs) == 0)
return 1;
else
return (NumArguments<FirstArg>() + NumArguments<RemainingArgs...>());
}
class A {
public:
A() {}
};
int main() {
std::cout << NumArguments<A>();
return 0;
}
Sadly I couldn't quite get a is_std_array
concept to work, but in terms of your NumArguments<T...>()
, it could be done with fold expression pretty easily:
template<typename ...T>
int NumArguments()
{
return (FuncReturnSize<T>() + ...);
}
The fold expression here will be expanded like:
return (((FuncReturnSize<T1>() + FuncReturnSize<T2>()) + FuncReturnSize<T3>()) + FuncReturnSize<T4>)
Demo
Here I specialized std::integral
and std::floating_point
version of FuncReturnSize()
, and any other types would just return sizeof(T)
. And you should be able to easily specialize other types with a good concept defined.
Note I also made FuncReturnSize()
s consteval
.
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