【问题标题】:Template factorial function without template specialization没有模板特化的模板阶乘函数
【发布时间】:2020-01-02 00:31:00
【问题描述】:

我不理解以下行为。

以下代码,旨在在编译时计算阶乘,甚至无法编译:

#include <iostream>
using namespace std;
template<int N>
int f() {
  if (N == 1) return 1; // we exit the recursion at 1 instead of 0
  return N*f<N-1>();
}
int main() {
  cout << f<5>() << endl;
  return 0;
}

并抛出以下错误:

...$ g++ factorial.cpp && ./a.out 
factorial.cpp: In instantiation of ‘int f() [with int N = -894]’:
factorial.cpp:7:18:   recursively required from ‘int f() [with int N = 4]’
factorial.cpp:7:18:   required from ‘int f() [with int N = 5]’
factorial.cpp:15:16:   required from here
factorial.cpp:7:18: fatal error: template instantiation depth exceeds maximum of 900 (use ‘-ftemplate-depth=’ to increase the maximum)
    7 |   return N*f<N-1>();
      |            ~~~~~~^~
compilation terminated.

然而,在添加 N == 0 的特化(上面的模板甚至没有达到)时,

template<>
int f<0>() {
  cout << "Hello, I'm the specialization.\n";
  return 1;
}

即使从未使用特化,代码也会编译并给出正确的输出:

...$ g++ factorial.cpp && ./a.out 
120

【问题讨论】:

  • 如果它可以可能被调用,它必须存在。
  • 在这种情况下,constexpr int f(int N);(或 c++20 中的 consteval)也可以。
  • 旁注:f&lt;-1&gt;() 的结果是什么?由于它没有意义,我更喜欢 unsigned int 作为模板参数。我们不会阻止任何人写f&lt;-1&gt;(无论如何都会被转换为大整数),但至少我们会从一开始就表示我们实际上只期望非负值......
  • 您得到了一个很好的答案,我无法有效地添加。我只想说明这是创建constexpr 的原因之一。
  • 数学完整:0 的阶乘定义为 1,所以你应该有 if constexpr(N == 0) return 1; else ...

标签: c++ templates recursion template-specialization factorial


【解决方案1】:

这里的问题是您的 if 语句是一个运行时构造。当你有

int f() {
  if (N == 1) return 1; // we exit the recursion at 1 instead of 0
  return N*f<N-1>();
}

f&lt;N-1&gt; 被实例化,因为它可能被调用。即使 if 条件会阻止它调用f&lt;0&gt;,编译器仍然必须实例化它,因为它是函数的一部分。这意味着它实例化了f&lt;4&gt;,它实例化了f&lt;3&gt;,它实例化了f&lt;2&gt;,并且它会一直持续下去。

Pre C++17 阻止这种情况的方法是使用 0 的特化来打破该链。从带有 constexpr if 的 C++17 开始,不再需要它。使用

int f() {
  if constexpr (N == 1) return 1; // we exit the recursion at 1 instead of 0
  else return N*f<N-1>();
}

保证return N*f&lt;N-1&gt;(); 甚至不会存在于1 案例中,因此您不会继续陷入实例化兔子洞。

【讨论】:

    【解决方案2】:

    问题在于f&lt;N&gt;() 总是 实例化f&lt;N-1&gt;(),无论是否采用分支。除非正确终止,否则将在编译时创建无限递归(即,它将尝试实例化 F&lt;0&gt;,然后是 f&lt;-1&gt;,然后是 f&lt;-2&gt; 等等)。显然,您应该以某种方式终止该递归。

    除了 NathanOliver 建议的 constexpr 解决方案和专业化之外,您可以显式终止递归:

    template <int N>
    inline int f()
    {
        if (N <= 1)
            return 1;
        return N * f<(N <= 1) ? N : N - 1>();
    }
    

    请注意,这个解决方案相当糟糕(相同的终端条件必须重复两次),我写这个答案只是为了表明总有更多的方法可以解决这个问题:-)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多