Trampoline In C++ - C++ Programming Tutorial
C++ Course / Miscellaneous / Trampoline In C++

Trampoline In C++

BLUF: Mastering Trampoline In C++ is a critical step in becoming a proficient C++ developer. This lesson provides a deep dive into the syntax, performance considerations, and real-world applications of this concept.
Key Performance Insight: Trampoline In C++

C++ is renowned for its efficiency. Learn how Trampoline In C++ enables low-level control and high-performance computing in the tutorial below.

In C++, trampolining is a specialized method primarily used to improve the efficiency of recursive function invocations. Recursive functions play a crucial role in simplifying complex problems into smaller, manageable ones. Nonetheless, excessive recursion can lead to drawbacks like exceeding the call stack limit and triggering a stack overflow error. Trampolining serves as a valuable solution in such scenarios.

The utilization of a trampoline enables the transformation of recursion into an iterative process, preventing the accumulation of values on the stack and consequently avoiding stack overflow. This approach simplifies the recursive logic while ensuring the continuous execution of the computation. The fundamental idea behind trampolining is to replace direct recursion with the return of another function that signifies the subsequent computational step, often in the form of a function object or a similar representation like a lambda or function pointer. Subsequently, these objects are iterated to simulate recursion within the main loop.

It can be beneficial when employed in conjunction with a tail-recursive invocation since the recursive call serves as the final operation executed within the function. There is no assurance that a specific compiler will automatically optimize tail recursion using tail call optimization (TCO). Trampolining provides a manual technique to accomplish a similar outcome by managing function calls in the heap rather than the stack.

Approach 1: Basic Function Pointers/Lambdas Trampoline

As mentioned, the Basic Function Pointers/Lambdas Trampoline is a straightforward approach to incorporating trampolining in C++. This recursive technique involves evaluating a process that promptly delivers a function object, like a lambda or function pointer, pointing to the subsequent step of the recursive function rather than executing the recursion directly. Subsequently, the trampoline system iterates the received function object repeatedly until achieving the desired outcome, effectively mitigating deep recursion and the possibility of a stack overflow.

Program:

Let's consider an example to demonstrate the use of the trampoline in C++.

Example

#include <iostream>
#include <variant>
#include <functional>
#include <stdexcept>
// Struct to represent a completed factorial calculation (result)
struct Done {
    int result;
};
// Struct to represent the next recursive step
struct Step {
    std::function<std::variant<Done, Step>()> next;
};
// Function that computes a step of the factorial function using the trampolining approach
std::variant<Done, Step> factorial_step(int n, int acc) {
    // Input validation: Factorial is only defined for non-negative integers
    if (n < 0) {
        throw std::invalid_argument("Factorial is not defined for negative numbers.");
    }
    // Step tracking: Print the current recursive state
    std::cout << "Computing factorial_step(" << n << ", " << acc << ")" << std::endl;
    // Base case: If n is 0, return Done with the accumulated result
    if (n == 0) {
        std::cout << "Base case reached with result = " << acc << std::endl;
        return Done{ acc };
    }
    // Recursive case: Return a Step that encapsulates the next recursive step as a lambda
    return Step{
        [=]() {
            // Next step in the recursion: decrement n and multiply acc by n
            return factorial_step(n - 1, n * acc);
        }
    };
}
// The trampoline function that keeps calling the recursive steps iteratively
int trampoline(std::variant<Done, Step> func) {
    // Initialize step counter to track the number of iterations
    int steps = 0;
    // Loop to evaluate the recursive steps iteratively, simulating recursion
    while (true) {
        // Check if the variant holds a Done value (i.e., the base case result)
        if (auto* done = std::get_if<Done>(&func)) {
            // Print the final result and return it
            std::cout << "Final result after " << steps << " steps: " << done->result << std::endl;
            return done->result;
        }
        // Check if the variant holds a Step value (i.e., more recursion steps to process)
        else if (auto* step = std::get_if<Step>(&func)) {
            // Print the current step number
            std::cout << "Step " << ++steps << ": Invoking the next recursive step" << std::endl;
            // Call the next step (the lambda) and update the function variant
            func = step->next();
        }
    }
}
// Function to compute the factorial using the trampoline mechanism
void compute_factorial(int number) {
    try {
        // Call the factorial_step function to initiate the recursive process
        auto result = trampoline(factorial_step(number, 1));
        // Print the computed factorial result
        std::cout << "Factorial of " << number << " is " << result << std::endl;
    } catch (const std::invalid_argument& e) {
        // Handle invalid input (like negative numbers)
        std::cerr << "Error: " << e.what() << std::endl;
    }
}
// Main function to test the factorial trampoline approach
int main() {
    // Test with a valid number (Factorial of 5)
    std::cout << "\nFactorial of 5:" << std::endl;
    compute_factorial(5);
    // Test with another valid number (Factorial of 7)
    std::cout << "\nFactorial of 7:" << std::endl;
    compute_factorial(7);
    // Test with 0 (Factorial of 0 is 1, base case)
    std::cout << "\nFactorial of 0:" << std::endl;
    compute_factorial(0);
    // Test with a negative number to trigger error handling
    std::cout << "\nFactorial of -3 (error case):" << std::endl;
    compute_factorial(-3);
    return 0;
}

Output:

Output

Factorial of 5:
Computing factorial_step(5, 1)
Step 1: Invoking the next recursive step
Computing factorial_step(4, 5)
Step 2: Invoking the next recursive step
Computing factorial_step(3, 20)
Step 3: Invoking the next recursive step
Computing factorial_step(2, 60)
Step 4: Invoking the next recursive step
Computing factorial_step(1, 120)
Step 5: Invoking the next recursive step
Computing factorial_step(0, 120)
Base case reached with result = 120
Final result after 5 steps: 120
Factorial of 5 is 120
Factorial of 7:
Computing factorial_step(7, 1)
Step 1: Invoking the next recursive step
Computing factorial_step(6, 7)
Step 2: Invoking the next recursive step
Computing factorial_step(5, 42)
Step 3: Invoking the next recursive step
Computing factorial_step(4, 210)
Step 4: Invoking the next recursive step
Computing factorial_step(3, 840)
Step 5: Invoking the next recursive step
Computing factorial_step(2, 2520)
Step 6: Invoking the next recursive step
Computing factorial_step(1, 5040)
Step 7: Invoking the next recursive step
Computing factorial_step(0, 5040)
Base case reached with result = 5040
Final result after 7 steps: 5040
Factorial of 7 is 5040
Factorial of 0:
Computing factorial_step(0, 1)
Base case reached with result = 1
Final result after 0 steps: 1
Factorial of 0 is 1
Factorial of -3 (error case):
ERROR!
Error: Factorial is not defined for negative numbers.

Explanation:

The provided code illustrates a trampoline-based recursive operation using C++ functionalities such as std::function and lambda expressions. In this instance, we will apply the variant, std::function, and lambda techniques to compute the factorial of a given number in an iterative fashion. This strategy helps avoid excessive recursion by breaking down the recursive operation into distinct phases, which are executed in a loop instead of recursively calling functions, potentially causing stack overflow issues.

This method helps avoid stack overflow problems while simplifying the implementation of recursive logic, a common feature in many programming languages. It also includes validation of input and provides a thorough explanation of the step-by-step calculation process to improve the reliability and clarity of the computation.

The step struct surrounds a function (as a lambda) that computes how to proceed to the next recursive step. A local head has two structs that allow the function to switch between a recursive step and the final yield.

  • Std::variant<Done, Step>: A std::variant is used to represent either the Done type or Step type because recursion is used and one step can return a Done, which indicates that the recursion is complete while another step can return a Step, indicating that more recursion is necessary. This variant is used to mimic recursion in a loop rather than the call stack, which is important when loop percentage recurs.
  • Factorial Step Function (factorialstep): The factorialstep function is an ordinary recursive factorial function used in calculating the factorial of a number. It is not a direct call to this function. Instead, it returns either a Done call when the base case was reached, i.e., n == 0, or a Step call, which contains a lambda for the next recursive step. Every call to this function either returns a lambda that wraps the top-level logic (in the case of Step) or the final accumulator (in the case of Done).
  • Trampoline Function: The trampoline function controls the evaluation process through the repeated application of the given recursion. It takes the first call from factorial_step. After that, it checks whether it is Done or if there are more steps to be processed in the Step.

When encountering a Step, it triggers the lambda function stored within the Step to retrieve the next Step or a result. This approach replaces traditional recursive call stack usage with an iterative loop, continuing until the Done signal is reached, concluding with the return of the final value.

  • Exception Handling: The var means method's functionality is contingent on receiving positive integer inputs for the factorial operation. Should a negative number be provided to the factorialstep function, a std::Argument: System.InvalidArgument exception is thrown, managed by the computefactorial call which directs the output to display an error message.
  • Step Tracking: Focused on debugging practices and computational processes, this segment incorporates various actions to log the calculated values at each recursion level. By illustrating how recursion progresses from one n-level to another, with the final step marking the end point, it also logs the total number of iterations before reaching the outcome. This logging mechanism aids in visualizing the control flow sequence, facilitating performance monitoring and optimization.
  • Complexity Analysis:

Time Complexity:

The time complexity of this code snippet is evaluated based on the quantity of recursive invocations an algorithm necessitates to calculate the factorial of a given number n.

Factorial Computation:

As highlighted in the 'factorial_step' function, a recursive approach involves decreasing the value of n at each iteration until it reaches 0 as the base case.

It's crucial to understand that each recursive call for a given number n will result in exactly n steps. Subsequently, we can describe the time complexity of the creation process in the following manner:

There are two actions involved in the construction process at step i, including a multiplication operation (n*acc) that requires constant time, specifically O(1).

Therefore, the quantity of recursive iterations, and consequently, the quantity of multiplication operations, equals n.

Trampoline Function:

The trampoline function iterates over each of the n recursive stages by invoking the lambda functions stored within the Step structure.

As there are a total of n recursive procedures to handle in the worst-case scenario, the time complexity is also O(n) due to the necessity of invoking a lambda function and modifying a state during the process.

Therefore, it has been determined that the time complexity for calculating the factorial with this trampoline approach is O(n) since the trampoline goes through n iterations, with each iteration requiring a consistent amount of time.

Space Complexity:

The time complexity is explained based on the number of computational steps, whereas the space complexity refers to the storage required for variables and data structures utilized during the computation process.

Recursive Call Handling:

Typically, a recursive function is anticipated to consume O(n) space on the call stack as each recursive invocation generates a fresh frame on the stack.

In reality, the recursive process we faced in the earlier trampoline strategy is transformed into an iterative approach through the employment of a loop. The space efficiency remains at O(1) as the trampoline function operates without recursion but instead utilizes a loop for execution completion.

Memory for std::variant and Lambda:

To depict every recursive iteration, a Step object is established with an enclosed lambda function. These lambdas capture values by value, generating a new lambda for each individual step that retains the existing values of 'n' and 'acc'. As there are 'n' steps resulting in the creation of a lambda at each step, the space complexity could be O(n) to accommodate all the lambdas. Subsequently, each lambda preserves data related to the ongoing step, specifically the integer values of 'n' and 'acc'. Hence, minimal space is required for each lambda to function effectively.

Variant Storage:

Here, a std::variant stores either a Done or Step object at any moment, but not simultaneously. This results in constant memory usage, represented as O(1) since the variant only requires space for one of the two structs.

Therefore, the memory usage of the code increases to O(n) as it necessitates the storage of n lambdas, with each lambda containing details on the recurring step. Nonetheless, since these lambdas are generated on the heap rather than the call stack, the code is immune to stack overflow errors.

Input Required

This code uses input(). Please provide values below:

Logic Practice
Install Logic Practice
Add to home screen for a faster app-like experience