Lambdas in C++

A change of pace from the usual series on Design Patterns and a second post for the day, a topic I found interesting and useful for both projects as well as solving problems for interviews is Lambas. While excessive usage will make your code unreadable and unmaneagable, in moderation lambdas are an extremely versatile tool to have in your arsenal.

Lambda functions, also known as anonymous functions or function literals depending on which programming language you have experience in, are functions that do not have a defined identifier. Based on the research by the mathematician Alonzo Church, Lambda Calculus or λ-calculus is a Turing Complete language. Lambda expressions are a staple of functional programming languages such as Haskell and Lisp (and dialects).

For example, in Javascript and Typescript this would be an anonymous function

var myFunction = function() {
    console.log("This is an anonymous function!");
}

Due to the idiosyncrasies of Javascript, functions are objects and can be assigned to a variable but notice, the function itself does not have a name. If you want to learn more about JS functions then I strongly recommend checking out MDN.

The gist of this post is available as a short cheatsheet which I have shared on Linkedin and can be viewed here.

Why would you want to use a lambda?

Quite often you would come across requiring a function that would be of use once, say while using a sorting algorithm that is a part of the standard library of the language you are using. You can extend the sort function to be able to do more complicated sorts or switch the type of sort from ascending to descending. For example in Python, here's how you could use a lambda and the standard sort utility sorted to sort a list of heights which are strings instead of integers.

heights = ["5'10", "5'11", "5'9", "5'5", "5'4", "5'7"]

#Sorted the heights based on the inches 
sorted_heights = sorted(heights, key=lambda x: int(x[2:]))

print(sorted_heights)
# ["5'4", "5'5", "5'7", "5'9", "5'10", "5'11"]

As you can see, we use a lambda to function to extend the functionality of the vanilla sorted() function in Python to make it more powerful. We stripped the initial height in feet and then typecast the remainder of the string to an int which the sort function can parse without issues. Back to the crux of the topic, C++ lambda functions work in a similar fashion.

The Anatomy of a Lambda Function

Lambda functions were introduced in C++11 and more functionality has been added in each major function. For the purposes of this post we will focus on the main features as of C++14, any features from C++20 will be labelled as such.

The overall anatomy of a lambda looks as follows:

[capture](parameters) modifiers -> return-type {body}
Anatomy of a Lambda

As you can see there are several broad parts which at a glance seem to be confusing but they can be rewritten in a manner that is similar to a more commonly used structure, a struct.

struct Lambda
{
	Lambda (char capture) : capture{capture} {}
	size_t my_function()(const char *parameter) const {
		//content of the lambda
	}
	private:
		const char capture;
};
Analogous Representation in the form of a Struct

If one were to map the components of the lambda to the this struct then the private character variable capture would be the capture of the lambda function, the parameters of the lambda being the parameter variable that is being passed to my_function. The modifier to this function would be const that is marking the function as a const. The return type of the function would be the size_t type and finally the body would be the commented out body of the function my_function. Let's take a look at each component in depth.

Captures

The first part of a C++ lambda is a capture. Enclosed in [], captures are effectively the member variables of the function. There are some restrictions on what you can or can not capture. You can not capture, for example, a variable with a thread local storage duration. Furthermore, you are not required to capture static variables as you can access them directly from the body of the lambda. The captures can be captured by referred or by value. The default behaviour is to capture by value. To capture by reference simply add a & in front of the capture.

#include <iostream>

using namespace std;



int main()
{
	int counter{5};
	cout<<"Original Value: "<<counter<<endl;
	auto reference_increment = [&counter](){ return counter++; };
	reference_increment();
	cout<<"By named reference: "<<counter<<endl;
	auto value_counter = [counter]() mutable { return counter++; };
	value_counter();
    cout<<"By named value: "<<counter<<endl;
}

// Original Value: 5
// By named reference: 6
// By named value: 6
Named Captures

The above example shows us capturing the counter variable first by reference and second by value. You may notice the mutable modifier in use, we will get to modifiers in a bit but this is because without using this modifier we will capture a read only copy of the variables, this means the compiler will throw a fit when we try to modify them. Using the mutable keyword means we create a deep copy of the variable instead.

While these were named captures there is a second type of capture called a default capture. What a default capture does is, it captures all auto variables within the scope of the lambda from the outer scope. To default capture by reference you use [&] and to default capture by value you can use [=]. Let us have a look at the same program, this time with default captures instead.

#include <iostream>

using namespace std;

int main()
{
	int counter{5};
	cout<<"Original Value: "<<counter<<endl;
	auto reference_increment = [&](){ return counter++; };
	reference_increment();
	cout<<"By default reference: "<<counter<<endl;
	auto value_counter = [=]() mutable { return counter++; };
	value_counter();
    cout<<"By default value: "<<counter<<endl;
}

// Original Value: 5
// By default reference: 6
// By default value: 6
Default Captures

Parameters

As with any functions you would expect to be able to pass parameters to the function. As with regular functions, you are able to pass a variable (or several) to the lambdas.

[](float r) {return 3.14*r*r}
Lambda function to return the area of a circle

The above is a very basic lambda function that accepts the parameter r which is the radius and then calculates and returns the area of the circle with said radius. Note how we have not specified most of the other components that were listed above, this is because all components of the lambda save the captures and the body are optional.

We can even have default parameters as we would have them in a regular function.

#include <iostream>

using namespace std;

int main() {
	auto decrement = [](int x, int y=1){ return x - y; };
	cout<<"Single Decrement: "<<decrement(10)<<endl;  
	cout<<"Double Decrement: "<<decrement(10,2)<<endl;
}

// Single Decrement: 8
// Double Decrement: 9
Lambda Function with default parameters

The above is an example of a lambda function with default parameters. When the second parameter y is not passed, the default value of 1 will be used. If a second parameter is passed then the function will ignore the default parameters and used the passed one instead as shown in the example above.

There are some additional functionalities that captures have. Namely, you can initialize a new variable to store a captured variable. Alternatively, if the lambda is enclosed in an object you can capture [this] or [*this].

Modifiers

These are specifiers like the aforementioned mutable or other types you can modify your functions with like noreturn, noexcept, etc.

Return Type

As the name this is used to set the return type of the function as the syntax is -> return-type. The example of this functionality is included in the above example where we sorted the students based on their grades in a descending order. We used the bool return type.

This is an optional field where are just having the compiler enforce the return type on our behalf instead of relying on the logic of the program to return the appropriate data type. While cumbersome, this is form of strongly static typing is beneficial in a larger codebases where you want to maintain some form of sanity and avoid causing bugs.

You can use decltype which is commonly used with Generic Lambdas, which we will talk about later in this piece.

Body

This is body of the function and is similar to the body of an actual function itself. You can enter the functionality you want the lambda expression to have in the body.

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

class Student {
	public:
		Student (string name, int grades): name{name}, grades{grades} {};
		string name;
		int grades;
};

int main()
{
	Student A{"John", 76}, B{"Adam", 94}, C{"Tony", 50};
    vector<Student> v{A, B, C};
	sort(v.begin(), v.end(), [](const Student A, const Student B)->bool{return A.grades>B.grades;});
	cout<<"Students: ";
	for(auto v: v){
	    cout <<v.name<<", ";
	}
}

// Students: Adam, John, Tony

The above code extends functionality of the regular std::sort to enable it to sort the objects of a class based on the values of one of their members, grades.

Generic Lambdas

While the above features are usable in C++11, generic lambdas are a feature exclusive to C++14 or higher. What are generic lambdas? They are expression templates where you can use auto for the function parameters instead of a concrete type like int or float.

#include <iostream>


using namespace std;

int main()
{
	int x{10};
	float y{0.5};
	auto square = [](auto x){return x*x;};
	int x_square = square(x);
	float y_square = square(y);
	cout << "Square of x: " << x_square << std::endl;
    cout << "Square of y: " << y_square << std::endl;
}

// Square of x: 100
// Square of y: 0.25

Thanks to the auto keyword, the function is incredibly flexible.

Closing Thoughts

This post should give you a broad understanding of how lambdas work in C++. If you want to go even deeper into Lambdas (and C++ overall) then I recommend reading up CPP reference which is an excellent reference for the language.

As always I would love to hear your feedback on this post, feel free to write to me at akshay [at] akshayprabhu [dot] dev or tweet at my twitter handle @akshaprabhu200.