17

I don't understand the following behavior.

The following code, aimed at computing the factorial at compile time, doesn't even compile:

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

and throws the following error:

...$ 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.

whereas, upon adding the specialization for N == 0 (which the template above doesn't even reach),

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

the code compiles and give the correct output of, even if the specialization is never used,

...$ g++ factorial.cpp && ./a.out 
120
Enlico
  • 23,259
  • 6
  • 48
  • 102
  • If it can *potentially* be called, it needs to exist. – Jesper Juhl Aug 28 '19 at 18:30
  • 2
    In this case `constexpr int f(int N);` (Or `consteval` in c++20) would also work. – Artyer Aug 28 '19 at 18:33
  • 1
    Sidenote: What would be the result of `f<-1>()`? As it is meaningless, I'd prefer unsigned int as template parameter. We wouldn't prevent anybody from writing `f<-1>` (would be converted to huge integer anyway), but at least we'd express right from the start that we actually expect non-negative values only... – Aconcagua Aug 28 '19 at 19:10
  • You've gotten an excellent answer that I cannot usefully add to. I just want to state that this is one of the reasons `constexpr` was created. – Omnifarious Aug 28 '19 at 19:24
  • To be *mathematically* complete: Factorial of 0 is defined as 1, so you should have `if constexpr(N == 0) return 1; else ...` – Aconcagua Aug 31 '19 at 07:16

2 Answers2

24

The issue here is that your if statement is a run time construct. When you have

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

the f<N-1> is instantiated as it may be called. Even though the if condition will stop it from calling f<0>, the compiler still has to instantiate it since it is part of the function. That means it instantiates f<4>, which instantiates f<3>, which instantiates f<2>, and on and on it will go forever.

The Pre C++17 way to stop this is to use a specialization for 0 which breaks that chain. Starting in C++17 with constexpr if, this is no longer needed. Using

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

guarantees that return N*f<N-1>(); won't even exist in the 1 case, so you don't keep going down the instantiation rabbit hole.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
5

The problem is that f<N>() always instantiates f<N-1>() whether that if branch is taken or not. Unless properly terminated, that would create infinite recursion at compile time (i.e. it would attempt instantiating F<0>, then f<-1>, then f<-2> and so on). Obviously you should terminate that recursion somehow.

Apart from constexpr solution and specialization suggested by NathanOliver, you may terminate the recursion explicitly:

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

Mind, this solution is rather poor (the same terminal condition must be repeated twice), I'm writing this answer merely to show that there are always more ways to solve the problem :- )

Igor G
  • 1,838
  • 6
  • 18