Making Your C++ Functions More Generic

As a developer, one of the principles that are hammered into your mind when you start out is DRY which means Do not Repeat Yourself, and it means exactly what it says… don’t repeat code. The idea is that if you find yourself writing the same lines of code at different places in your codebase, you must start thinking about bundling that piece of code up into a function, class, library or something else that will make your code organised and cleaner. This article looks at a feature in C++ programming called lambda expression and how it can help you to make generic functions that can be reused throughout applications.

Prerequisites

Before we start, of course, there are some things you need to know. First, you need a functioning knowledge of C/C++. You must be comfortable reading, understanding and writing C++ code. Also, you need some IDE or environment that you can write and compile C++ code in. I use VSCode and I will be releasing a guide on how to set up VSCode for C++ development soon. Stay tuned.

Contents of this article

This article will go over some basic concepts in C/C++ like Functions and Pointers to serve as a build-up to the concept of Function Pointers, a precursor to Lambda expressions. Then we will tackle Lambdas, a method of creating disposable functions at the point where they are needed and how they can make your functions more generic. Excited yet? Let's get started with Functions…

Functions - What are they?

If you have some experience with programming, you must be familiar with functions. The formal C++ reference defines functions as “entities that associate a sequence of statements with a name and a list of zero or more parameters”. Functions are designed to help programmers group blocks of code that perform a specific task together in one place which could then be called at different points in a project. An example function could be:

double calculateDensity(double mass, double volume)
{
    return mass * volume;
}

Function declarations and definitions are always preceded by the type that they return or void if they do not return anything. Formally functions serve as the most primitive way of upholding the DRY concept. They help programmers prevent code duplication throughout their code by grouping sequences of statements that perform a specific task in one place.

Pointers - “I know a guy”

Pointers are special types of variables that “point” to a place in memory or hold the address of a memory location. Computer memory is organised in such a way that every memory location has an address which allows the computer to know where to store and retrieve data effectively. Pointers do not hold the value at that specific place in memory, it just holds the address. A perfect analogy would be the character of Saul Goodman in the Breaking Bad universe. Although people look for him to offer services aside from his lawyering duties, he does not render these services by himself but rather knows a guy who can do it. And that's what pointers are, they know the memory location that has the data you are looking for. Declaring pointers is as simple as prefixing an identifier with an asterisk(*), like this:

double *memory_location_of_mass = mass;

In this example case, the pointer memory_location_of_mass does not contain the value of mass, but the address or the memory location where the value of mass is stored at.

Function Pointers

A function pointer is a special type of pointer that holds the memory location of a function. The memory location of a function? I thought pointers only stored data and not code. Well, the truth is that functions themselves are also data; they live somewhere inside computer memory until they are called. The instructions that constitute a function are stored at a memory location with an address. Function pointers hold the address of the instructions that constitute the function. This opens the door for opportunities like storing a function in a variable and passing it to other functions as an argument. Remember the function we defined earlier? We can store it in a function pointer by simply writing:

//1
double(*func_pointer)(double, double) = calculateDensity;

OR

//2
auto func_pointer = calculateDensity;

(1) shows the actual declaration of a function pointer which is quite complicated whiles (2) uses the auto keyword because of its ability to infer types automatically (Get where the name comes from?).

Lambdas

Lambdas are anonymous functions. What does that even mean? They are simply functions that do not have names. They are usually written inline at the point where they are needed. You may ask “Where would I need something like this?”.

There are many instances where lambdas are useful but in the context of this article, we might want to use a lambda to create a function that takes in another function as a parameter. Let me explain:

Suppose I want to write a function that calculates the density of an object. That function would simply look like this ->

double calculateDensity(double mass, double volume)
{
    return mass / volume;
}

Now suppose in the program, there are different ways that the mass of an object is obtained. First, as a variable that contains the quantity itself:

double first_object_mass = 74.0;

Or as part of another quantity say weight:

double second_object_weight = 740.0;

Now we want to write a function that can take either of the quantities' mass and weight and still give an accurate result of the density of the object. The most obvious way is to write another function that converts the weight into the mass of the object and then passes it into the density function:

double calculateMassFromWeight(double weight)
{
    return weight / 10;  // I am assuming that the acceleration due to gravity is 10m/s
}

We then use this function in our program like this;

#include <iostream>
int main()
{
    double volume = 100;
    double first_object_mass = 74.0;
    double second_object_weight = 740.0;
    double first_object_density = calculateDensity(mass, volume);
    double second_object_mass = calculateMassFromWeight(second_object_weight );
    double second_object_density = calculateDensity(second_object_mass , volume);
    std::cout << “Density of first object:” << first_object_density <<std::endl;
    std::cout << “Density of second object:” << second_object_density <<std::endl;
}

But what if there are other quantities (like inertia and energy) in our code base that have the mass of an object and we need to calculate this mass on the fly for use in our calculations? Well this means that we need to declare and define each of those functions and then use it in our code:

double calculateMassFromInertia(double inertia, double distance)
{
    return inertia / (distance * distance);
}
double calculateMassFromEnergy(double energy, double speed_of_light)
{
    return energy / ( speed_of_light * speed_of_light);
}

These functions are then used in our program:

int main()
{
    // Pardon the repeating print statements. This is for demonstration purposes only
    double volume = 100;
    double distance = 100;
    double speed_of_light = 300000000;
    double first_object_mass = 74.0;
    double second_object_weight = 740.0;
    double third_object_inertia = 740.0;
    double fourth_object_energy = 6.66e18;

    double first_object_density = calculateDensity(first_object_mass , volume);
    std::cout << “Density of first object:” << first_object_density <<std::endl;

    double second_object_mass = calculateMassFromWeight(second_object_weight );
    double second_object_density = calculateDensity(second_object_mass , volume);
    std::cout << “Density of second object:” << second_object_density <<std::endl;

    double third_object_mass = calculateMassFromInertia(third_object_inertia, distance);
    double third_object_density = calculateDensity(third_object_mass, volume);
    std::cout << “Density of third object:” << third_object_density <<std::endl;

    double fourth_object_mass = calculateMassFromEnergy(fourth_object_energy, speed_of_light);
    double fourth_object_density = calculateDensity(fourth_object_mass, volume);
    std::cout << “Density of fourth object:” << fourth_object_density <<std::endl;
}

But what if we wanted to make calculateDensity generic? In our codebase, multiple functions calculate for mass out of some quantity they are given. Instead of passing in just mass and volume after calculating for the mass, what if we pass in the function itself? This is where Lambdas come in. Lambdas allows us to create throwaway functions at the point that they are needed. To do this, we could change the parameters of the density function to have a volume parameter and another parameter that represents the function that we will be passing in. To do this we need to include “functional.h” into our code. After doing that we can move on to modifying our density function to be:

double genericCalculateDensity(double volume, std::function<double()> &calculateMass, std::function<void(double)> &logValue )
{
    double mass = calculateMass();
    double density = mass / volume;
    logValue(density);
    return density;
}

Let me explain… “std::function<double()>” is the type for the calculateMass function. The bits in the <angular brackets> represent the input and output data types of this throwaway lambda function that we will create further down the line. At this point, we don’t know the internals of this function but we know that it will return a double value representing the mass of the object. The identifier calculateMass is just an identifier that will allow us to use the incoming function in our new generic function. Throwing in another yet-to-be-defined function for good measure, we add another function that will not return anything but takes an input value of type “double(std::function<void(double)>). It is aptly named logValue to indicate that it is going to be a function that logs the results of the calculation somewhere, either to the screen or to a file or to any other output device. How do we define these lambda functions that will be passed into the generic calculateDensity function? As an example, we will write out the definition for just one of these functions. Here’s the syntax for a typical lambda function:

[capture_variables](parameters)
{
    /*function body*/
}

Lambda functions have their scope and cannot access any other property outside their scope. However, there are ways that we can make the lambda functions “capture” the values of properties outside its scope and this is done by using the capture variables. The capture braces can contain various symbols which determine what gets captured into the lambda function. Leaving the braces empty means that nothing is captured into the lambda function. We can capture properties outside the scope of the lambda function either by value (using the equals sign ‘=’) or by reference (using an ampersand ‘&’). In our case, we would like to get the specific value we need for our lambdas by reference therefore we will prefix the property that we want with an ampersand. Here’s our partly completed lambda function for calculating the mass of an object from its energy:

[&fourth_object_energy, &speed_of_light](parameters)
{
    /* function body */
}

Since we are not taking in any parameters, we leave the parameters bracket empty. We then write our code which calculates the mass from the energy of an object into the function body:

[&fourth_object_energy, &speed_of_light]()
{
    return fourth_object_energy / (speed_of_light * speed_of_light);
}

With that, we are done writing one of the lambda functions that we need for our mass calculations (It is at this point where I highly recommend that you look at cppreference.com documentation on lambdas for more). Substituting these generic functions into our sample program and adding the other bits and bobs, we now have:

int main()
{
    double volume = 100;
    double distance = 100;
    double speed_of_light = 300000000;
    double first_object_mass = 74.0;
    double second_object_weight = 740.0;
    double third_object_inertia = 740.0;
    double fourth_object_energy = 6.66e18;

    auto logLambda = [](double value)
                     { 
                         std::cout << "Density of object :" << value << std::endl; 
                     };

    genericCalculateDensity( volume, 
                           [&first_object_mass]()
                           { 
                               return first_object_mass; 
                           },
                           logLambda);

    genericCalculateDensity( volume, 
                           [&second_object_weight]()
                           { 
                               return second_object_weight / 10; 
                           },        
                           logLambda);

    genericCalculateDensity( volume, 
                           [&third_object_inertia, &distance]()
                           { 
                               return third_object_inertia / (distance * distance); 
                           },
                           logLambda);

    genericCalculateDensity( volume, 
                           [&fourth_object_energy, &speed_of_light]()
                           { 
                               return fourth_object_energy / (speed_of_light * speed_of_light);        
                           },
                           logLambda);
}

As you can see another lambda function “logLambda” is defined and introduced as the second parameter for the genericCalculateDensity function. This function is present to log the results of the density calculations to the console screen. With the generic density calculation function done, it is obvious to see from the four instances that it is called, a different lambda function is passed in which calculates the mass of the object from some specified set of parameters and then that value is then finally used in the density calculation. Making the density calculation more generic means that we can pass different functions with the same signature as the calculateMass parameter and expect the same behaviour from all of them.

Conclusion

This article aimed to inform readers and exposed them to some concepts that can make their code much cleaner and more concise by making their functions much more generic. Along the way, some concepts relevant to the article and how they fit into its context are explained. There is more to explore concerning these concepts and if you want to go deeper, I suggest you take a look at The Cherno’s youtube videos and also read the cppreference.com documentation. Leave a comment if you enjoyed this article. Cheers!