为没有模板参数的 Variadic Template 递归创建一个基本案例
Creating a base case for Variadic Template recursion with no template arguments
我正在尝试对可变参数模板使用递归。我希望基本情况具有零模板参数。通过Whosebug对之前问题的回答,我发现了两种对这个问题的回答:
- 您不应该特化模板函数。 Herb Sutter 在这里写道:http://www.gotw.ca/publications/mill17.htm
- 您使用
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...>();
}
Args
中 Arg
的可能列表是有限的,并且在编译之前已知。 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>)
这里我特化了 std::integral
和 std::floating_point
版本的 FuncReturnSize()
,任何其他类型都只是 return sizeof(T)
。并且您应该能够通过定义好的概念轻松地专门化其他类型。
注意我也做了 FuncReturnSize()
s consteval
.
我正在尝试对可变参数模板使用递归。我希望基本情况具有零模板参数。通过Whosebug对之前问题的回答,我发现了两种对这个问题的回答:
- 您不应该特化模板函数。 Herb Sutter 在这里写道:http://www.gotw.ca/publications/mill17.htm
- 您使用
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...>();
}
Args
中 Arg
的可能列表是有限的,并且在编译之前已知。 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>)
这里我特化了 std::integral
和 std::floating_point
版本的 FuncReturnSize()
,任何其他类型都只是 return sizeof(T)
。并且您应该能够通过定义好的概念轻松地专门化其他类型。
注意我也做了 FuncReturnSize()
s consteval
.