Search code examples
c++recursionvariadic-templatesc++20c++-concepts

Using concepts to detect empty parameter packs


In an answer to another question I posted, Jack Harwood shared a nice solution to detect empty variadic parameter packs using concepts. The example problem is to compute the number of parameter pack arguments using recursion. I reproduce his solution below.

template <typename... Args>
concept NonVoidArgs = sizeof...(Args) > 0;

template <typename... Args>
concept VoidArgs = sizeof...(Args) == 0;

template <VoidArgs...>
constexpr int NumArguments() {
    return 0;
}

template<typename FirstArg, NonVoidArgs... RemainingArgs>
constexpr int NumArguments() {
    return 1 + NumArguments<RemainingArgs...>();
}

Example:

int main() {
    std::cout << NumArguments<int>() << std::endl; // 1
    std::cout << NumArguments() << std::endl; // 0
    std::cout << NumArguments<float, int, double, char>() << std::endl; // 4
    return 0;
}

I think that this is a better solution than using class templates to specialize function templates. However, I'm not sure why it works. The template function

template<typename FirstArg, NonVoidArgs... RemainingArgs>
constexpr int NumArguments()

seems to require at least two template arguments. There needs to be a FirstArg and then at least 1 RemainingArgs. Why does the compiler call this overload(?) when there is only one template argument? Does this behavior create any problems with this solution?


Solution

  • The concepts themselves are correct but the problem is that the example uses them incorrectly and is itself faulty. In the given code the concept is imposed on an argument basis instead of the entire parameter pack.


    Your current version

    template<typename FirstArg, NonVoidArgs... RemainingArgs>
    constexpr int NumArguments() {
      return 1 + NumArguments<RemainingArgs...>();
    }
    

    is actually equivalent to a requires clause and a fold expression

    template<typename FirstArg, typename... RemainingArgs>
    int NumArguments() requires (NonVoidArgs<RemainingArgs> && ...) {
      return 1 + NumArguments<RemainingArgs...>();
    }
    

    This actually means you are defining a function that must have an argument FirstArg and might have an additional parameter pack of arbitrary size RemainingArgs (including zero!). Each of these arguments - if present - is then checked independently if it fulfills the NonVoidArgs concept which of course is always true. This means the function basically degenerates to

    template<typename FirstArg, typename... RemainingArgs>
    int NumArguments() {
      return 1 + NumArguments<RemainingArgs...>();
    }
    

    That being said it is actually not even necessary for the parameter pack to be NonVoidArgs: Try it here!

    The correct way to impose restrictions not only on the data type of a single template argument but the entire parameter pack would actually be a requires clause as follows:

    template<typename FirstArg, typename... RemainingArgs>
    int NumArguments() requires NonVoidArgs<RemainingArgs...> {
      return 1 + NumArguments<RemainingArgs...>();
    }
    

    As expected this would not work: Try it here! I guess the person who has written the code was just lucky it actually works as they did not enforce the concepts correctly. The basic idea behind the concept is flawed.