Modern C++ In-Depth — Lambdas, Part 3

Michael Kristofik
FactSet
Published in
3 min readFeb 1, 2023

This week, we conclude our three-part series on one of the more popular features introduced in C++11, lambda expressions. In our last installment, we examined how lambdas work and how they help simplify code. Now let’s build on that knowledge to see how we might store a collection of lambda expressions.

Storing Lambdas

Suppose we want to declare a container to store a number of function objects. How might we go about doing this? Using a std::vector<T> seems like a reasonable first step, but what should our template parameter T be?

Since the compiler generates a unique type for each lambda expression that we write, it would seem that we have a problem. It’s not immediately obvious how we might declare a vector that can hold more than a single lambda.

Fortunately, we don’t really need to know the exact type of each lambda that we’re trying to store. The only thing the compiler needs to know about each of our lambda expressions is how to invoke it. Somewhat more precisely, the compiler only needs to know the function signature of the callable type that we’re trying to store. With that signature in hand, the compiler can determine whether a particular invocation of the callable type is valid.

The standard library type that allows us to “erase” the exact type of our callable object, while preserving the function signature needed to maintain type safety, is std::function<T>.

In order to use std::function<T>, we have to provide the function signature of the enclosed type as a template parameter. This signature generally takes the form R (Args...), where R specifies the return type and (Args...) enumerates each of the formal parameter types needed to invoke the callable object. If the callable type doesn’t take any input, we can use R () instead.

Let’s turn to an example. If we want to wrap a lambda expression that takes two const std::string& parameters as input and returns the length of their concatenation, we could write:

const std::function<std::string::size_type (const std::string&, const std::string&)> fn =
[](const std::string& lhs, const std::string& rhs) {
return lhs.size() + rhs.size(); // Returns a `std::string::size_type`
};

Building on this, here is how we might create and consume a collection of heterogeneous function objects:

int main()
{
std::vector<std::function<int (int, int)>> binary_operations;

binary_operations.emplace_back([](int lhs, int rhs){ return lhs + rhs; });
binary_operations.emplace_back([](int lhs, int rhs){ return lhs - rhs; });
binary_operations.emplace_back([](int lhs, int rhs){ return lhs * rhs; });
binary_operations.emplace_back([](int lhs, int rhs){ return lhs / rhs; });

for (const auto& fn : binary_operations) {
std::cout << fn(10, 2) << '\n';
}
}

After iterating through the vector, we’ll be left with the following output:

12
8
20
5

Limitations

While std::function allows us to solve a variety of problems, there are some limitations.

Due to implementation constraints, a std::function is not capable of storing a lambda whose closure contains a move-only type. Consider the following:

// A move-only type:
struct widget
{
widget() = default;

widget(const widget&) = delete;
widget& operator=(const widget&) = delete;

widget(widget&&) = default;
widget& operator=(widget&&) = default;
};

int main()
{
const std::function<void()> fn = [state = widget()] { /*...*/ };
}

Since our widget cannot be copied (only moved), the snippet above will fail to compile. In most cases, this issue can be avoided by placing the move-only type on the heap using a (copyable) std::shared_ptr:

int main()
{
const std::function<void()> fn = [state = std::make_shared<widget>()] { /*...*/ };
}

Developers should also be aware that lambdas with large closures will be heap-allocated once wrapped within a std::function, and that this may incur a slight performance penalty.

What’s Next?

This concludes our three-part series on lambda expressions. For the previous posts in the series, please see our introduction to lambdas, and a deeper dive into lambda syntax in part two.

Next time, we’ll be exploring user-defined literals.

Acknowledgments

Special thanks to all who contributed to this blog post.

Author: Tim Severeijns
Reviewers: Michael Kristofik and Jens Maurer

--

--

Michael Kristofik
FactSet
Editor for

Principal Software Architect at FactSet. I post on behalf of our company's C++ Guidance Group.