C++ Lambdas

Lambdas are the new addition to C++ in the latest revision C++11. Lambdas allow us to define small, anonymous function objects right where we need them. Lambdas solve the complexities of the Function Pointers and Functors. This also encourages functional programming in C++.

Functional programming is a style of programming that focuses on evaluating functions and working with data that never changes.

Let’s start with the complexities and verbosity of Function Pointers and Functors.

Function Pointers

A core idea in this paradigm is that you can pass one function into another as an argument. One practical way to do that is by using a function pointer.

Just like objects, functions live in memory, and since they are in memory, we can refer their location with a pointer. The difference is that we can change the object properties using the pointer referring to the object, but the function’s internal code can’t be modified through function pointer.

To declare a function pointer, use the following syntax:

1
return_type (*name)(type1 arg1, type2 arg2, ...);

Code Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

int add(int a, int b) {
return a + b;
}

// apply will accept a function pointer
// that should match definition of functions like `add`
int apply(int (*func)(int, int), int x, int y) {
return func(x, y);
}

int main() {
int result = apply(add, 3, 4);
std::cout << "Result: " << result << '\n';
}

Use Cases

The good use cases of function pointers are:

  • Speed up loops — pick one function once, avoid repeated condition checks inside tight loops.
  • Write callback systems — let a library or device call functions when something happens.
  • Implement state machines — each state is just a pointer to the function that handles it.

State machines are interesting and we must explore it.

Functors

A Functor, technically function object is simply a C++ class or struct that acts like a function. We can call it using the same syntax as a regular function. Functors were formalized and standardized in C++98 is any object that overloads the function call operator - ().

Code Example

To create a functor, we define a class and implement operator().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

class Multiplier {
int factor;
public:
// Constructor captures state
Multiplier(int f) : factor(f) {}

// Overloading the function call operator
int operator()(int x) {
return x * factor;
}
};

int main() {
Multiplier timesFive(5);

std::cout << timesFive(10);
return 0;
}

Functors were designed to solve specific limitations associated with raw function pointers. Let’s get to know the issues.

State Problem

Regular functions & function pointers are stateless. If we want a function to remember a value, we have to use global variables or static variables, which leads to messy code and are not thread safe.

In the above code we can see that the function can have members and hence can store state.

Performance Issues

When we pass a standard function pointer to an algorithm like std::sort, the compiler generally cannot inline that call. The compiler has to make an indirect function call via a pointer address at runtime, this prevents optimization and slows down tight loops.

Functors are passed by type. Since the specific class type is known at compile time, the compiler can look inside operator() and inline the code directly.

Signature Flexibility

Imagine an STL algorithm that will only call a function with one argument. But our logic needs two args like the value and a setting we want to compare against. A function pointer can’t help you here because it must match the exact signature.

A functor, however, solves the problem neatly. We tuck that extra setting into the functor when we create it,
and when the algorithm calls operator() with just one argument, the functor quietly uses the stored setting behind the scenes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <vector>
#include <algorithm>

struct GreaterThan {
int limit;
GreaterThan(int l) : limit(l) {}

bool operator()(int x) const {
return x > limit;
}
};

int main() {
std::vector<int> v {1, 5, 3, 8, 2};
auto it = std::find_if(v.begin(), v.end(), GreaterThan(4));
std::cout << *it;
}

Lambdas

C++ 11 introduced Lambdas which are actually a concise way to create anonymous functors right where we need them. They are inline function definitions, making code cleaner and more expressive, especially when used with STL algorithms.

Structure of lambdas

1
[capture_list](parameters) -> return type { function body }

C++11 lambdas are essentially syntactic sugar for Functors. When we write a lambda, the compiler actually generates a unique, unnamed functor class for you in the background.

Code Example

Let’s have a look at the simples lambda function:

1
2
3
4
5
6
7
8
9
#include <iostream>

int main() {
[]() {
std::cout << "Hello, I am the simplest lambda.\n";
}();

return 0;
}

Another example,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

int main() {
int factor = 5;

// [factor] captures the external 'factor' by value.
auto multiplier = [factor](int x) -> int {
return x * factor;
};

// calling the lambda like a function
int result = multiplier(10);

std::cout << "10 multiplied by " << factor << " is: " << result << "\n";

return 0;
}

Capturing Variables

When we define a lambda, we can either specify which variables to capture, or let it automatically capture the variables in scope by copy or by reference. A lambda can capture specific variables we choose, or automatically capture all nearby variables by copying or referencing them.

1
2
auto f = [=]() { return limit * factor; };  // capture everything by value
auto g = [&]() { counter++; limit++; }; // capture everything by reference
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

int main() {
int limit = 5;
int factor = 2;
int counter = 0;

// captures by variables by value i.e. generating copies
auto f = [=]() {
return limit * factor; // this will be 10
};

// captures by reference
auto g = [&]() {
counter++; // modifies counter
limit++; // modifies limit
};

std::cout << f() << std::endl; // prints 10

g();

std::cout << counter << std::endl; // prints 1
std::cout << limit << std::endl; // prints 6
}

Closing Notes

So we explored function pointers, how functors solves the problems and improves performance and finally saw how C++ 11 makes it even easier to encourage functional programming via lambdas. In short, we can conclude that C++11 Lambdas makes it easy to write high-performance, stateful, and context-aware code without sacrificing conciseness.

So when you are switching to C++ lambdas?

Happy coding and take care.