使用概念检测空参数包

Using concepts to detect empty parameter packs

在我发布的 to 中,Jack Harwood 分享了一个使用概念检测空可变参数包的很好的解决方案。示例问题是使用递归计算参数包参数的数量。我在下面重现了他的解决方案。

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...>();
}

示例:

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;
}

我认为这是比使用 class 模板专门化函数模板更好的解决方案。但是,我不确定它为什么有效。模板函数

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

似乎 需要至少两个模板参数。需要有一个 FirstArg,然后至少有 1 个 RemainingArgs。当只有一个模板参数时,为什么编译器会调用这个重载(?)?此行为是否会对该解决方案造成任何问题?

这些概念本身是正确的,但问题是示例使用不正确并且本身是错误的。 在给定的代码中,概念是基于参数而不是整个参数包。


您当前的版本

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

实际上相当于一个requires子句和一个折叠表达式

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

这实际上意味着您定义的函数必须有一个参数 FirstArg,并且可能有一个任意大小的附加参数包 RemainingArgs(包括零!)。这些参数中的每一个 - 如果存在 - 如果满足 NonVoidArgs 概念,则将被独立检查,这当然总是正确的。这意味着函数基本上退化为

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

话虽如此,参数包实际上什至不需要NonVoidArgsTry it here!

不仅对单个模板参数的数据类型而且对整个参数包施加限制的正确方法实际上是一个 requires 子句,如下所示:

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

正如预期的那样,这是行不通的:Try it here! 我想编写代码的人很幸运,因为他们没有正确执行这些概念,所以它实际上可以工作。这个概念背后的基本思想是有缺陷的。