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

Phani Adusumilli
FactSet
Published in
4 min readNov 29, 2022

This week, we begin a three-part series on one of the more popular features introduced in C++11, namely lambdas. For companies like ours with a large legacy codebase, this may be the first time developers have been able to use modern C++. We’re sharing what we’ve learned with the wider C++ community in the hopes that others will find it as useful as we have.

While lambdas do not allow us to write anything we could not have written
before their introduction, they greatly improve the ease with which we can
write function objects and callbacks.

Motivation

Tucked away within the standard library is the algorithms library. It offers developers a wide variety of utility functions that operate on ranges of elements. These functions can be used for anything from sorting and searching to data transformation.

For instance, if we have a collection of tickers that we want to sort in
lexicographical order, we can accomplish that with a simple one-liner:

std::vector<std::string> tickers =
{ "FDS", "AAPL", "TSLA", "X", "GOOG", "F", "BA" };
std::sort(std::begin(tickers), std::end(tickers));

The std::sort function compares and swaps two elements at a time
to ensure that smaller elements appear before larger ones. Since we only
provided two arguments to the std::sort function in the example above, the default sorting predicate will be used to reorder the collection. For strings this results in a lexicographical ordering.

Now let's suppose we want to sort these tickers based on, say, the opening
price. To do this, we will need to supply a custom comparison predicate as the third parameter to the std::sort function. Since this predicate is expected to be a function-like object, we can either pass along a free-standing function, or an object that defines the function call operator.

Suppose we determine that our sorting task is best accomplished by taking
advantage of cached opening prices. Here’s what that might look like prior to C++11:

class ticker_cache
{
public:
double get_opening_price(const std::string& ticker) const;
};

const ticker_cache& get_cache();

class price_comparator
{
public:
price_comparator(const ticker_cache& cache) : m_cache(cache)
{
}

bool operator()(const std::string& lhs,
const std::string& rhs) const
{
return m_cache.get_opening_price(lhs) <
m_cache.get_opening_price(rhs);
}

private:
const ticker_cache& m_cache;
};

void sort_by_price(std::vector<std::string>& tickers)
{
std::sort(std::begin(tickers), std::end(tickers),
price_comparator(get_cache()));
}

The price_comparator class is an example of a function object, or functor.
Since the class defines a function call operator (i.e., operator()), we are
able to call an instance of this class as if it were an ordinary function.
Here's an isolated example:

// Construct an instance called 'foo'
price_comparator foo(get_cache());

// Call 'foo' as if it were a function
const bool is_correctly_ordered = foo("AAPL", "TSLA");

As alluded to earlier, using a hand-written functor is not the only way to
accomplish our goal. The same sorting task can also be tackled using additional library functionality. Here’s an example that uses boost::bind (since std::bind does not exist prior to C++11):

bool compare_price(const ticker_cache& cache,
const std::string& lhs,
const std::string& rhs)
{
return cache.get_opening_price(lhs) <
cache.get_opening_price(rhs);
}

void sort_by_price(std::vector<std::string>& tickers)
{
using namespace boost::placeholders;
const auto& cache = get_cache();
std::sort(std::begin(tickers), std::end(tickers),
boost::bind(&compare_price, boost::cref(cache), _1, _2));
}

The complexity of the technique and syntax depends a lot on where the
comparison function resides; it requires more effort to invoke a member
function than it does to call a free-standing, stateless function.

There is one more issue to consider. All of these solutions require the
comparison logic to be implemented outside of the call to std::sort. Anyone reading our code will have to navigate away from the current function (possibly to a different file) to figure out what's going on. Having to search for code makes it harder to read and understand.

Using a Lambda Instead

The issues of syntactic complexity and code locality can all be mitigated by
using a lambda expression:

void lambda_demo(std::vector<std::string>& tickers)
{
const auto& cache = get_cache();

std::sort(std::begin(tickers), std::end(tickers),
[&cache](const std::string& lhs, const std::string& rhs) {
return cache.get_opening_price(lhs) <
cache.get_opening_price(rhs);
});
}

The third parameter in our std::sort call constitutes the lambda expression. This syntax allows us to define a function object that looks and behaves almost exactly like our earlier price_comparator class. We can even capture surrounding variables for use within the lambda body.

Most lambda expressions will have three parts: a capture list, a
parameter list, and a function body.

The square brackets allow us to define which variables from the surrounding scope will be captured for use within the function body; in this case, we will explicitly capture cache by reference. There are a variety of different options and pitfalls to be aware of when capturing variables, and we'll cover those in detail in our next installment.

The parentheses allow us to define the formal parameters required
to invoke the lambda. These parameters are identical to those used for the
function call operator seen in the earlier price_comparator example.

Lastly, the braces allow us to define the function body of the lambda
expression. This, too, is analogous to the body of the function call operator
of the price_comparator class.

What’s Next?

In the next installment, we’ll take a closer look at how lambdas work under the hood, how one can create closures to capture state, and what to look out for.

Acknowledgments

Special thanks to all that contributed to this blog post:

Author: Tim Severeijns
Reviewers: James Abbatiello, Michael Kristofik, Jens Maurer

Originally published at https://medium.com on November 29, 2022.

--

--

Phani Adusumilli
FactSet
Writer for

I’m an architect who is passionate about distributed systems, APIs, Data Lakes, and Data Mesh in Financial Services