为没有模板参数的 Variadic Template 递归创建一个基本案例

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

我正在尝试对可变参数模板使用递归。我希望基本情况具有零模板参数。通过Whosebug对之前问题的回答,我发现了两种对这个问题的回答:

  1. 您不应该特化模板函数。 Herb Sutter 在这里写道:http://www.gotw.ca/publications/mill17.htm
  2. 您使用 template <typename = void> template <typename T = void> 。比如这里的第一个答案:

我试图在我的问题中使用解决方案 (2),但收到错误。这是一个最小的、可重现的例子:

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

在 Microsoft Visual C++20 中编译出现错误:

 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 '()'

这条错误信息是什么意思?如何使用可变参数模板为递归创建 zero-argument 基本案例?

编辑:评论中有人要求更完整地描述我的问题。问题确实是问题标题,而不是“如何让我的代码工作?”,但我还没有编译我的代码,所以我决定分享它。

NumArguments 是另一个函数 ComputeSize 的 stand-in,它将 Args 作为输入,return 是 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...>(); 
    }

ArgsArg 的可能列表是有限的,并且在编译之前已知。 FuncReturnSize 对于这些 Args 中的每一个都超载了。例如,两个可能的“重载”(?)是

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

概念is_std_array_concept<> 应该检查t.Func() 的return 值是否是某个大小的数组。我还不确定它是否有效。它由

定义
    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;

我希望所有这些计算都在 compile-time 完成,所以我定义了

template<std::size_t N>
std::size_t CompilerCompute() {
    return N;
}

我现在应该可以在编译时 ComputeSize 像这样:

CompilerCompute<ComputeSize<Args...>()>()

错误消息的意思与它所说的完全一样,调用不明确。

template<typename = void> // base case
constexpr int NumArguments() {
    return 0;
}

这不是采用 0 个参数的模板函数,这是采用一个默认参数的模板函数(因此,如果未指定参数,则该参数无效)。这意味着 NumArguments<A>() 是对此函数的完全有效调用。

但是,NumArguments<A>() 也是对带有空可变参数包的可变参数重载的完全有效调用(错误消息中列出的 NumArguments<A,>() 重载)。

使您的案例与链接示例不同的是,在链接示例中,可变参数重载是在 int 上而不是类型上模板化的,因此那里没有歧义。我在此处复制了该实现:

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

注意,f 的第二个重载是一个可变参数模板,其中每个模板参数必须是一个 int 值。如果 A 是类型,调用 f<A>() 将不会匹配该重载,因此避免了歧义。

不可能声明零参数模板函数,所以你运气不好。但是,您可以将其转换为 class 模板,因为 class 模板可以部分专门化。

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

这个具体实现当然可以简化为使用 sizeof...,但 OP 表示他们的实际用例更复杂。

您应该保证在不重载函数的情况下结束可变参数模板。

使用 c++ 标准 17(在 Microsoft Visual /std:c++17 中)编译的解决方案如下:

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

这是另一个解决方案(没有专门化),它使用 C++20 requires 子句来解决歧义:

template <typename... Args> requires (sizeof...(Args) == 0)
constexpr int NumArguments() {
    return 0;
}

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

示例:

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

编辑: 我以前使用 concepts 的建议是不正确的。关于使用概念和参数包有很好的 post

遗憾的是,我无法完全理解 is_std_array 的概念,但就您的 NumArguments<T...>() 而言,可以很容易地使用折叠表达式来完成:

template<typename ...T>
int NumArguments()
{
    return (FuncReturnSize<T>() + ...);
}

此处的折叠表达式将展开为:

return (((FuncReturnSize<T1>() + FuncReturnSize<T2>()) + FuncReturnSize<T3>()) + FuncReturnSize<T4>)

Demo

这里我特化了 std::integralstd::floating_point 版本的 FuncReturnSize(),任何其他类型都只是 return sizeof(T)。并且您应该能够通过定义好的概念轻松地专门化其他类型。

注意我也做了 FuncReturnSize()s consteval.