Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating a base case for Variadic Template recursion with no template arguments

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:

  1. You should not specialize templates functions. Herb Sutter wrote about that here: http://www.gotw.ca/publications/mill17.htm
  2. You use 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 Args 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...>()>()
like image 667
mana Avatar asked Jun 18 '21 20:06

mana


Video Answer


4 Answers

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 ints, 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.

like image 125
Kyle Avatar answered Oct 24 '22 05:10

Kyle


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.

like image 26
Jack Harwood Avatar answered Oct 24 '22 05:10

Jack Harwood


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;
}
like image 1
José D. Tascón-Vidarte Avatar answered Oct 24 '22 07:10

José D. Tascón-Vidarte


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.

like image 1
Ranoiaetep Avatar answered Oct 24 '22 07:10

Ranoiaetep