Recursion Primer Using C++: Part 1

Introduction

In general, recursion means self repeating patterns. In Mathematics, it can be a function that is defined in terms of itself, such as factorial, Fibonacci etc. In computer programming, recursion means a function that is defined in terms of itself. In other words, a function that calls itself. Every recursive function has a termination condition; otherwise, it will call itself forever, and this condition can be called the base condition.

Usually, recursion is a little bit difficult to understand for most students. Researchers usually come up with different strategies to teach recursion easily for students, such as teach recursion before teaching arrays [4], delegations [2], or recursively generated geometric designs [3].

Types of Recursion

In C++, the types of recursion can be defined in more than one dimension. In one dimension, it can be categorized as runtime recursion and compile time recursion using template meta-programming.

Runtime recursion is the most common recursion technique used in C++. This can be implemented when a C++ function (or member function) calls itself.

In C++, we can also do compile time recursion with the help of template meta-programming. When you instantiate a template class (or structure) in C++, the compiler will create the code of that class at compile time. Just like runtime recursion, we can instantiate the template class itself to perform the recursion. Just like runtime recursion, we also need the termination condition; otherwise, instantiation will go forever, at least theoretically, but, of course, limited to the resources of the computer and the compiler. In template meta-programming, we can specify the termination condition (or base condition) with the help of template specialization or partial template specialization, depending on the termination condition.

One can think that we might do the same thing with the preprocessor of C++, using macros, because they will also be replaced during compilation. In fact, technically, preprocessor replaces all macros even before compilation, so it is not performing at compile time. The preprocessor also has lots of limitations like, there is no debug symbol defined for the debugger because of simple text replacement, but the most critical limitation is it can not be recursive. The section 16.3.4.2 of the Standard of C++ [1] strictly restricts writing macros that call themselves recursively, therefore we can’t do recursive programming using macros just like we do in templates.

The other way to look at recursion is how a recursive algorithm is implemented. Recursive algorithms can be implemented in more than one way, such as linear, tail, mutual, binary, or nested recursion. We can implement them either at compile time using template meta-programming, or at runtime using functions or member functions.

We can represent the different types of recursion using the following diagram. This diagram shows the different types of recursion based on their implementation (i.e., linear, tail, mutual etc.) and when it will be performed.

Now, we are going to explore the different types of algorithms one by one, and take a look at their runtime as well as compile time implementations.

Linear Recursion

Linear recursion is the simplest form of recursion, and perhaps the most commonly used. In this recursion, a function simply calls itself until it reaches the termination condition (also known as the base condition); this process is known as winding. After calling the termination condition, the execution of the program returns to the caller; this is known as unwinding.

Functions may perform some additional tasks during winding or unwinding, such as in the case of the factorial function, it will multiply the input number with the return value of the function during the unwinding phase. This process can be demonstrated with the following diagram that shows both the winding and unwinding phase of the factorial function using linear recursion.

Mathematically, we can write the factorial function recursively this way, i.e., when the value of “n” is zero, then return one, and when the value of “n” is greater than zero, then call the function recursively with “n-1” and multiply the result with the return value of the recursive function.

The above program is a runtime version of linear recursion. Here, we have a termination condition of 0, and this program starts unwinding when it reaches the termination condition. There is one more error condition in this program to prevent infinite function calls if someone passes a negative number to this function. This function will simply return -1 as an error if the value of the parameter is negative.

This program is a compile time counter part of Program 1. Here, we use template instantiation to perform the recursion. We also have a termination condition in the form of a template specialization. This is quite a simple example of template specialization, and we do not have a lot of code, but in a few cases, we might have to rewrite all the code in the template specialization class (or structure) too, because we can’t inherit the code from the template class (or structure) to the specialization class (or structure).

Here, we didn’t even introduce the error condition of a negative number, because this program won't even compile if someone tries to call this with a negative number. This is one very big advantage of compile time recursion that we can’t compile the infinite calling condition. However, the error message is perhaps not very obvious in that case.

Here, the compiler will refuse to compile the last statement and give a long cryptic error message.

Tail Recursion

Tail recursion is a specialized form of linear recursion where the recursive function call is usually the last call of the function. This type of recursion is usually more efficient because a smart compiler will automatically convert this recursion into a loop to avoid nested function calls. Because a recursive function call is usually the last statement of a function, there isn’t any work done during the unwinding phase; instead, they simply return the value of the recursive function call. Here is an example of the same program converted to tail recursion.

We can define the tail recursion mathematically using this equation, i.e., when the value of “n” is zero, then simply return the value of “a”; if the value of “n” is greater than zero, then call the recursive function by passing “n-a” and “n*a”. Here, you can also notice that during the unwinding phase, every recursive function simply returns the value of “a”.

This is a modified version of the linear recursion program. Here, we perform all the calculations before calling the recursive function, and simply return whatever value we got from the recursive function. Here, the calculation order is the reverse of the linear recursion. In linear recursion, we first multiply 1 with 2, then its result with 3, and so on; on the other hand, here we multiply n with n-1, then with n-2, until we reach 0.

Here is the compile time version of the same program doing the same thing at compile time.

Tail recursion is very useful and some times unavoidable in functional languages, because they might not have a looping construct. They usually perform the looping with the help of tail recursion. You can do almost everything with tail recursion that can be done with looping, but this is usually not true in reverse. Here is a very simple example to demonstrate looping via tail recursion:

But, this program looks very rigid, and we can not customize it. With the help of templates and function objects, we can customize this function so you can control each individual piece of it. Here is a modified version of the same program where we can control each and every part of it.

We can not give the default implementation in the “RecursiveLoop” function because C++ doesn’t allot the default template parameter to the function template. We can do this only in the case of a C++ class. To overcome this situation, we can make a “RecursiveLoop” function object instead of a simple function and passe the default action as the default parameter, so its calling will be much simpler. Here is the revised version of the existing program:

Mutual Recursion

Mutual recursion is also known as indirect recursion. In this type of recursion, two or more functions call each other in a circular way. This is the only way of doing recursion if the programming language doesn’t allow calling functions recursively. The termination condition in this recursion can be in one or all of the functions.

This is the most primitive example of mutual recursion. We know that zero is an even number and 1 is an odd number. If we want to check whether a number is even or odd, then we can call these functions, which internally call each other by subtracting one from the input value until the base condition is reached. Of course, this is not the best way to implement this algorithm; it will take a lot of resources to check whether a number is even or odd. In addition, if someone passed a negative number, then they will keep calling each other, and eventually throw a stack overflow error at run time.

Here is the compile time version of the same program. The only difference between this program and the above one is that it will not even compile if we try to pass a negative number to it.

Determining whether a number is even or odd using mutual recursion is not a very good idea. A more interesting example is male sequence and female sequence. Both functions recursively call each other, and can be defined as:

Here is the runtime and compile time versions of the Male and Female functions, using mutual recursion:

Like other compile time recursion functions, we have to do template specialization for both functions to handle the termination condition.

Binary Recursion

In binary recursion, the recursive function calls itself twice, not once. This type of recursion is very useful in some data structures like traversing a tree in prefix, postfix, or infix order, generating Fibonacci numbers etc.

Binary recursion is a specific form of exponential recursion where one function calls the recursive function more than once (in case of binary, two). In other words, recursive functions call exponentially in this type of recursion.

Here is a simple implementation of Fibonacci numbers calling the recursive function twice. Here we have two base cases, when the value of input parameter is 1 and 2. This is, of course, not the best implementation of Fibonacci numbers, and we can convert it into tail recursion by changing it a little bit. But, before converting this one into tail recursion, take a look at the compile time version of binary recursion.

Here, we convert binary recursion into tail recursion. We simply perform the calculation before calling any recursive function, therefore we do not need to call the recursive function twice. In Fibonacci numbers, we always need the last two numbers, so after performing the calculation on the last two numbers, we just discard the first one, i.e., “a”, and replace the second one in place of the first, i.e., place the value of “b” in “a”, and calculate the next number.

Program 15: Compile time example of converting binary recursion into tail recursion

Here is the compile time version of this program. Here, we have only one termination condition (i.e., base case), therefore we need only one template specialization. Here, we perform partial template specialization because we want the last calculated value in the base case when the value of n is equal to 1.

Nested Recursion

This is a special type of recursion when the recursive call is nested. All of the above recursions can be replaced with either simple looping or looping with stack, but this type of recursion can not be easily replaced by a simple loop.

One typical example of nested recursion is the Ackermann function. Here is a simple diagram of the Ackermann function to demonstrate nested recursion. We explicitly select a small value as a parameter, for simplicity.

Here is the runtime implementation of nested recursion. This function has two termination conditions; one condition terminates the nested calls and starts performing linear recursion, and the other termination condition stops the linear recursion. The first “if” condition is for error checking.

In compile time version of nested call we have to do template specialization twice because of two termination condition. The first specialization stops the nested call and start doing linear recursion and the second specialization stops the linear recursion.

Template Recursion

All the programs we discussed above either run completely at compile time or at runtime. We can make a program that can use both compile time as well as time features. One such example is instantiating an object at compile time using compile time recursion and then using it at runtime. For example, if we want to create a class of multi-dimensional arrays, then we can pass the dimension, with type, as a parameter and create an object of the same class with one less dimension inside. When the dimension reaches one, we can stop it with template specialization and provide implementation of a one dimensional array. We can represent this concept with the following diagram.

Here is a simple implementation of this. Just to make it simple, I avoid const correctness, use std::vector, and give the minimum possible interface of this class.

However, I must add that recursion should be used sparingly, if ever, with an imperative language like C++. Reasons:

1) Stack overflow - unless you can prove that recursion will never go "too deep" you are running into danger of the stack overflow.
2) Performance - even if you know you would never hit the stack overflow, calling a function is not really a cheap operation, and with recursion inlining is impossible (except if the compiler can deduce it would be called a very limited number of times). Also, a C++ compiler is unable to do the tail recursion optimization.

In my opinion (please prove otherwise) it is allowed for a C++ compiler
to do tail recursion optimization as long as can be proved that the effect
of the program is the same as without this optimization.

Furthermore, traditionally, one uses recursion in C++ only when there is
not a trivial iterative program which does the same as the recursive one.

Recursion only became indispensable in C++ with template metagprogramming,
since recursively defined templates make templates computationally complete (meaning that arbitrary algorithms can be implemented with templates and hence arbitrary computations are possible at compile time).

it is allowed for a C++ compiler
to do tail recursion optimization as long as can be proved that the effect
of the program is the same as without this optimization.

It is allowed, but very hard to achieve[^] and without inspecting the generated machine code you could never know for sure whether the optimization actually took place.

Don't get me wrong, there are languages where recursion is not only safe and useful, but also mandatory - C and C++ are not among them. There is another good article about this[^] written by a former lead developer of KAI C/C++ compiler.