15B. Tail Recursion

There is a special kind of recursion where the cost of using recursion is much smaller than in the general case. Tail recursion is a situation where a recursive call is the last thing a function does before returning, and the function either returns the result of the recursive call or (when the return type is void) returns no result.

Look at the function definitions above. The recursive call in nextPrime is

   return nextPrime(n+1);  
That is tail recursion. The function does nothing with the result of the recursive call except to return it. The recursive call in the definition of member,
   return member(x, A, n-1);
is also tail recursion, as is the recursive call
   return allPositive(A, n-1);
in the definition of allPositive. Of course, there are non-tail recursive calls. The recursive call in the definition of sum on the previous page,
   return sum(A, n-1) + A[n-1];
is not tail-recursive. After getting the result sum(A, n−1), sum adds A[n−1] to that result.

So what is the significance of tail recursion? A compiler can recognize tail recursion and replace it by a loop, making it more efficient in both time and memory.

For illustration, let's convert the recursive definition of allPositive to a loop. Here is the starting point.

  bool allPositive(const int A[], const int n)
  {
    if(n == 0)
    {
      return true;
    }
    else if(A[n-1] <= 0)
    {
      return false;
    }
    else
    {
      return allPositive(A, n-1);
    }
  }

Because the recursive call is tail recursive, nothing in a given frame of allPositive needs to be remembered when the recursive call is done. After all, none of that information will be used. The compiler can safely convert it to changing the parameters and going back to the beginning.

  bool allPositive(const int A[], const int n)
  {
    while(true)
    {
      if(n == 0)
      {
        return true;
      }
      else if(A[n-1] <= 0)
      {
        return false;
      }
      else
      {
        n = n - 1;
	// continue the loop.
      }
    }
  }

You would not be allowed to change the value of n, since it is a const parameter. But the compiler is not bound by that restriction; it can do whatever it wants to do, as long as the translated program works correctly according to the rules of C++.

The nice thing about tail recursion is that you do not convert it to a loop; you can let the compiler do that. The recursive definition might be easier to write.

Most compilers only convert tail recursion to a loop when asked to optimize. The g++ compiler will do that conversion if you use command-line option -O.


Exercises

  1. What is the advantage of tail-recursion over general recursion? Answer

  2. Is the following definition of g tail-recursive?

      int g(int n)
      {
        if(n == 1) 
        {
           return 2;
        }
        else
        {
           return g(n-1) + 3;
        }
      }
    
    Answer

  3. Write a definition of factorialTimes(n, m), which returns m*n!. Make the definition use tail recursion. Answer