Lambdas: Typical use cases

My last post, highlighting the differences between C++ lambdas and the lambdas in languages based upon functional programs, went more deeply into the weeds than I had intended. I realized too late that it cried out for examples. The rules for declaring free variables (the member variables of the anonymous class) in C++ lambdas can seem contorted but most uses of them fall into one of a few simple types. In this post, I want to present three simple use cases of C++ lambdas, each demonstrating a different way to use free variables.

Copy-constructed constant values

Perhaps the most common use of free variables, in all languages, is to pass in some parameters to guide the lambda’s algorithm. For example, a filter function might be passed parameters specifying the filter range. Here’s a simple example of a min/max filter over an integer vector:

// Copy-construct const values
vector<int> filter (const vector<int>& vec, int minimum, int maximum) {
  vector<int> res {};
  copy_if (vec.begin(), vec.end(), back_inserter(res),
    [=](auto v) { return minimum <= v && v <= maximum; });
  return res;
}

The [=] lambda prefix specifies that all free variables in the lambda expression are copy-initialized from the surrounding context and never modified in the execution of the filter. The free variables minimum and maxiumum are initialized from the function parameters of the same name.

This pattern is so common that you could spend an entire career and prefix every single lambda you use with the [=] prefix.

Reference mutable variables

Another common use case (although less common than copy-constructed immutables) is to update variables in the surrounding context. In this example,

// Reference mutable values
int accum (const vector<int>& vec) {
  int sum = 0;
  for_each(vec.begin(), vec.end(),
    [&] (auto v) { sum += v; });
  return sum;
}

The [&] lambda prefix specifies that all free variables in the lambda expression will be references to correspondingly-named variables in their surrounding context. In this case, sum is used to retain the running accumulated sum across calls of the lambda. The function then returns the accumulated total.

By the way, if you actually want to accumulate values, it is preferable to use std::accumulate or std::reduce, which address this requirement directly. In particular, std::reduce has parallel variants that exploit multi-core and multi-processor architectures.

Mutable local variable

A less common use case is a lambda that needs a local variable. The lambda syntax permits the declaration of locals that do not correspond to any variables in the containing context. An example of such a case is a function that, given a vector, returns a vector of (index, value) tuples, similar to Python’s enumerate function:

// Mutable local
using vec_pair = vector< tuple<int,int> >;
vec_pair enumerate(const vector<int>& vec) {
  vec_pair res {};
  transform (vec.begin(), vec.end(), back_inserter(res),
    [ind=0] (auto v) mutable { return make_tuple (ind++, v); });
  return res;
}

The [ind=0] lambda prefix defines a local variable ind whose type and initial value is determined by =0. ind is a member variable of the lambda object, unneeded outside that context. Note that unlike the other two cases, there is no variable ind declared in the surrounding context. Note also the mutable declaration; lambdas with private local variables are virtually the only use case that you will need to declare the lambda mutable. (The use of the mutable keyword in lambdas is unrelated to its distinct use as a qualifier for non-static member variables of a class.)

More general cases

The full lambda syntax permits far more general definitions of member variables for the lambda object than these simple cases. I believe the overwhelming majority of uses fall into one of these types, though. You can go a long, long time without writing a lambda prefix more complicated than these three cases.

Summary

Although the full syntax of C++ lambdas is complex, nearly all use cases fall in one of a small set of simple cases. The syntax allows these cases to be expressed concisely, in a context (expressions) where concision is a real benefit. It does take some time to become sufficiently familiar with the possibilities to take advantage of these simple cases, though.