Lesson 2
Implementing a Function that Takes Another Function as an Argument
Lesson Introduction

In modern C++ programming, higher-order functions, which are functions that take other functions as arguments, are fundamental tools. They make your programs more flexible, reusable, and modular.

Our goal for this lesson is to learn how to implement a function that takes another function as an argument. This concept is crucial for custom algorithms and many standard library functions in C++. Ready to dive in? Let's start!

Understanding Function Arguments

Before we delve into the main topic, let's quickly recap function pointers and std::function from the <functional> library. These tools let us pass functions as arguments to other functions, adding flexibility to our code design.

  • Function Pointers: A function pointer is a variable that stores the address of a function, which can be called through it.
  • std::function: This modern tool encapsulates any callable target (functions, lambda expressions, bind expressions, or other function objects) with a specific signature.

Why pass functions as arguments? Imagine working on a vector of integers and needing to filter out certain elements. You can create a generic function that takes another function (the filter criterion) to handle the filtering. This avoids code duplication and makes your logic clear and concise.

Example Problem: Filter Elements from a Vector.

Let's see this in action by filtering elements from a vector. We will implement a filterVector function that takes a vector and another function, which defines the filtering rules:

C++
1#include <iostream> 2#include <vector> 3#include <algorithm> 4#include <functional> 5 6// Function that takes another function to filter vector elements 7void filterVector(const std::vector<int>& vec, std::function<bool(int)> filterFunc) { 8 std::vector<int> filteredVec; 9 for (int elem : vec) { 10 if (filterFunc(elem)) { 11 filteredVec.push_back(elem); 12 } 13 } 14 15 std::cout << "Filtered Elements: "; 16 for (int elem : filteredVec) { 17 std::cout << elem << ' '; 18 } 19 std::cout << '\n'; 20}

Let's break down the implementation:

  • Function Prototype:

    C++
    1void filterVector(const std::vector<int>& vec, std::function<bool(int)> filterFunc);

    This specifies that filterVector takes a vector of integers and a function returning a bool.

  • Vector Declaration:

    C++
    1std::vector<int> filteredVec;

    This declares a vector to store filtered elements.

  • For-Loop and Filter Function Call:

    Then, the loop iterates through each element of the input vector, applying filterFunc. If an element satisfies the condition, it is added to filteredVec. Finally, the filtered elements are printed.

Understanding `auto`

Before continuing, let's briefly discuss the auto keyword in C++. Introduced in C++11, the auto keyword allows the compiler to automatically deduce the type of a variable from its initializer. This makes your code cleaner and often easier to read by removing redundancy:

C++
1auto x = 10; // int 2auto y = 3.14; // double 3auto z = "Hello"; // const char*

When working with functions that return complex types, such as lambdas, auto can simplify variable declarations. This is especially useful when the exact type is cumbersome to write out.

Example Problem: Using Function

Let's look at an example of using the function implemented previously:

C++
1int main() { 2 std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; 3 4 // Filter even numbers 5 auto isEven = [](int n) { return n % 2 == 0; }; 6 filterVector(numbers, isEven); 7 // Output: Filtered Elements: 2 4 6 8 10 8 9 // Filter numbers greater than 5 10 auto greaterThanFive = [](int n) { return n > 5; }; 11 filterVector(numbers, greaterThanFive); 12 // Output: Filtered Elements: 6 7 8 9 10 13 14 return 0; 15}

In this main function:

  • We create a vector numbers with integers from 1 to 10.
  • We define the isEven lambda to identify even numbers. This will be our boolean function that defines the filtering rules. We use auto type to make the function creation easier.
  • We call filterVector with isEven to filter and print even numbers.
  • We define another lambda greaterThanFive to filter and print numbers greater than 5.
Handling Different Scenarios

Functions that take other functions as arguments are highly flexible. You can easily swap out the function argument to handle different scenarios. Here are more examples:

Filtering odd numbers:

C++
1auto isOdd = [](int n) { return n % 2 != 0; }; 2filterVector(numbers, isOdd); 3// Output: Filtered Elements: 1 3 5 7 9

Filtering numbers within a range:

C++
1auto withinRange = [](int n) { return n > 3 && n < 8; }; 2filterVector(numbers, withinRange); 3// Output: Filtered Elements: 4 5 6 7

Each example shows adapting filterVector to different needs by changing the function passed as an argument.

Pros and Cons of Higher-Order Functions

Pros:

  • Flexibility: Higher-order functions allow you to create flexible and reusable code. For example, filterVector can filter numbers based on various criteria without needing separate functions for each case.
  • Modularity: By passing functions as arguments, you can separate the logic for different tasks, making your code more modular.
  • Code Reduction: They help in reducing code duplication. Instead of writing similar functions for different filtering criteria, you write one generic function.

Cons:

  • Complexity: Understanding and debugging higher-order functions can be difficult for those new to the concept.
  • Compile-Time Errors: Because of the flexible nature of higher-order functions and type inference with auto, you might encounter more complex compile-time errors.
Lesson Summary

Great job! You've learned to implement a function in C++ that takes another function as an argument. This skill is invaluable for writing flexible, reusable, and modular code. We covered:

  • The importance and utility of higher-order functions.
  • Using std::function to pass functions as arguments.
  • Implementing a higher-order function with a practical example (filterVector).
  • Using lambda expressions for various scenarios.
  • The pros and cons of using higher-order functions.

Now it's time for hands-on practice! You'll implement your own functions that take other functions as arguments. These exercises will solidify your understanding and allow you to experiment with different use cases. Happy coding!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.