如何编写可变参数模板递归函数?

22

我正尝试编写一个可变参数模板constexpr函数,用于计算给定模板参数的总和。以下是我的代码:

template<int First, int... Rest>
constexpr int f()
{
    return First + f<Rest...>();
}

template<int First>
constexpr int f()
{
    return First;
}

int main()
{
    f<1, 2, 3>();
    return 0;
}

很不幸,尝试解析f<3,>()时出现错误信息error C2668: 'f': ambiguous call to overloaded function

我还尝试将递归的基本情况更改为接受0个模板参数而不是1个:

template<>
constexpr int f()
{
    return 0;
}

但是这段代码也无法编译(错误信息为error C2912: explicit specialization 'int f(void)' is not a specialization of a function template)。

我可以提取第一个和第二个模板参数以使其编译并正常工作,像这样:

template<int First, int Second, int... Rest>
constexpr int f()
{
    return First + f<Second, Rest...>();
}

但这似乎不是最好的选择。因此,问题是:如何以优雅的方式编写这个计算?

更新:我也尝试将其编写为单个函数:

template<int First, int... Rest>
constexpr int f()
{
    return sizeof...(Rest) == 0 ? First : (First + f<Rest...>());
}

而且这也不起作用:错误 C2672:未找到匹配的重载函数“f”


这有点多余,不是吗?如果它是一个constexpr,编译器允许在编译时评估表达式,那么为什么要将其作为模板呢?为什么不定义constexpr int f(int a, ...) - example
是的,在求和的情况下,这种方法是可行的,但在我的实际情况中,函数要复杂一些,并且需要知道 Rest... 的长度(类似于转换为另一个基数),这不能作为 constexpr 评估。 - alexeykuzmin0
6个回答

21

你的基本情况是错误的。你需要一个空列表的情况,但正如编译器建议的那样,你的第二次尝试不是一个有效的模板特化。定义零个参数的有效实例化的一种方法是创建一个重载,接受一个空列表。

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

编辑:为了完整起见,这是我的第一个答案,@alexeykuzmin0通过添加条件句进行了修复:

template<int First=0, int... Rest>
constexpr int f()
{
    return sizeof...(Rest)==0 ? First : First + f<Rest...>();
}

13
template<int First, int... Rest>
constexpr int f()
{
    return First + f<Rest...>();
}

template<int First>
constexpr int f()
{
    return First;
}

int main()
{
    f<1, 2, 3>();
    return 0;
}
您遇到了这个错误:
error C2668: 'f': ambiguous call to overloaded function while trying to resolve f<3,>() call.
由于可变参数包可以不传递任何参数,因此通过“展开”为template<3, >f<3>可以与template<int First, int... Rest>一起使用。但是,您也有template<int First>的特化版本,因此编译器不知道选择哪一个。

明确声明第一个和第二个模板参数是解决此问题的完全有效和好方法。

template <>
constexpr int f()
{
    return 0;
}

由于函数无法以那种方式进行特化,因此您会遇到问题。可以对类和结构进行特化,但是不能对函数进行特化。


解决方案#1:使用constexpr的C++17折叠表达式

template <typename... Is>
constexpr int sum(Is... values) {
    return (0 + ... + values);
}
解决方案 #2:使用constexpr函数。
constexpr int sum() {
    return 0;
}

template <typename I, typename... Is>
constexpr int sum(I first, Is... rest) {
    return first + sum(rest...);
}
解决方案#3:使用模板元编程。
template <int... Is>
struct sum;

template <>
struct sum<>
    : std::integral_constant<int, 0>
{};

template <int First, int... Rest>
struct sum<First, Rest...>
    : std::integral_constant<int,
        First + sum_impl<Rest...>::value>
{};

感谢您的详细解释。 - alexeykuzmin0
我一直在尝试使用字符串来完成这个任务,但好像无法成功地使用std::stringview进行constexpr,同时在元编程方面也失败了,是我太蠢还是这样做不可能呢? - Troyseph

10

我通常发现将代码从模板参数移到函数参数更容易:

constexpr int sum() { return 0; }

template <class T, class... Ts>
constexpr int sum(T value, Ts... rest) {
    return value + sum(rest...);
}

如果你真的想把它们作为模板参数,你可以让你的f函数通过将它们下移来调用sum函数:

template <int... Is>
constexpr int f() {
    return sum(Is...);
}

这个代码使用了constexpr,所以只用int就可以了。


是的,对于求和这种方法是可行的,但我的情况要复杂一些。由于问题表述不正确,我将关闭此问题。 - alexeykuzmin0

7

按照通常的方式简单总结。

template<int... Args>
constexpr int f() {
   int sum = 0;
   for(int i : { Args... }) sum += i;
   return sum;
}

这是我尝试的第一件事。不幸的是,我的编译器(MSVC++ 2015)无法理解constexpr函数中的变量声明。 - alexeykuzmin0
@alexeykuzmin0 我看到你更改了C++14标签。好的。 - T.C.
是的,我才意识到缺少这个功能意味着我的编译器不完全支持C++14。 - alexeykuzmin0
1
@alexeykuzmin0 改变问题要求,以至于使发布的答案无效,并不被普遍认为是礼貌的行为,因为它让人觉得发布答案的人犯了错误。 - jaggedSpire
1
@jaggedSpire 我理解这一点并非常抱歉。但是,让问题的表述不正确并不能帮助得到正确的答案。 - alexeykuzmin0
如果原始问题的动机是在构建时完成评估的大部分工作,那么这种方法的缺点是会生成动态代码(迭代for循环)。其他示例扩展为正确长度的显式总和,可以在没有动态迭代和循环检查/跳转的情况下进行处理。对于像整数求和这样的玩具示例,这似乎有点过度,但我想这个问题是实际问题的简单模型。 - HelpingHand

3
使用std::initializer_list的更通用的解决方案如下:

template <typename... V>                                                                                                                                                                                         
auto sum_all(V... v)
{
  using rettype = typename std::common_type_t<V...>;
  rettype result{};
  (void)std::initializer_list<int>{(result += v, 0)...};
  return result;
}

0

这与@T.C上面建议的相当相似:

#include<iostream>
#include<numeric>

template<int First, int... Rest>
constexpr int f()
{
    auto types = {Rest...};
    return std::accumulate(std::begin(types), std::end(types),0);   
}

int main()
{
    std::cout <<f<1, 2, 3>();
    return 0;
}

这需要使用C++14,而不是11。 - alexeykuzmin0
你不能在常量表达式中使用它。尝试使用constexpr int v = f<1,2,3>();,它不会编译。 - skypjack
我发现这引出了原问题,就像T.C.的回答一样。 - HelpingHand

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接