C++ Lambdas
May 29, 2016
9 minutes read

Lambdas don’t bring anything new to the language, everything they do can already be expressed using other language constructs. What they do provide and encourage is writing expressive and clean code. They are beautiful. The concept of anonymous functions is nothing new, they have been around since the inception of lambda calculus in the 1930’s and popularized in programming languages with Lisp in the 1950’s. They were added to C++ in the C++11 standard in 2011 as a primary expression and further extended in C++14.

Theory

In a nutshell a lamda expression provides a convenient way of creating a function object. They have some of the more esoteric syntaxes in C++ and takes some getting used to:

auto p = [=](int n) -> int { return n + 1; }(4);
// p = 5

(Note: here we’re invoking the lambda by applying () to the expression. Had we not done that ‘p’ would’ve held the closure object which is the result of evaluating the expresion, for later call like p(4))

That’s quite the mouthful. Forunately with return type deduction and generalized lambdas the syntax is somewhat more manageable. More formally a lambda expression is made up of:

[]      - lamdba introducer, contains (optional) capture list
()      - lambda declarator, contains lambda parameters (optional)
mutable - can modify copied variables (optional)
->      - return type (optional)
{}      - function body statements

The capture list determines how variables in the context of the lambda expression are made visible to the lambda function body.

[]      - capture nothing
[=]     - default-capture implicitly by value
[&]     - default-capture implicitly by reference
[n]     - capture 'n' explicitly by value
[=n]    - capture 'n' explicitly by value
[&n]    - capture 'n' explicitly by reference
[this]  - capture 'this' pointer by reference

[&n...] - capture pack expansion by reference
[=n...] - capture pack expansion by value

[=, &n] - by default captured by value, except 'n' which is
          captured by reference

[&, =n] - by default captured by reference, except 'n' which
          is captured by value

[p = std::move(p)] - init-capture 'p'

A brief segway into terminology. A lambda expression is what we see in code, like the example above. The compiler then generates a closure class, which is the internal function object definition. Evaluating the lambda expression yields a closure object, which is a temporary that can be stored in a named variable. The type of a lambda expression is closure type and is the same as that of the closure object. Each lambda has a different unique type. All of that is a lot of words for what can simply be thought of, and called, a lambda.

As mentioned, a lambda is internally represented as a compiler generated function object. For a lambda expression like this:

int p = 2;
[=p](int n) { return n * p };

the compiler will generate something like:

class __lambda0 {
public:
    __lambda0(int p) : __p(p) { }
    int operator()(int n) const { return n * __p; }

private:
    int __p;
};

This also helps to explain how the capturing of variables work. For each captured-by-copy variable the compiler will store a non-static data member inside the closure class. How captured-by-reference variables are stored is implementation specific.

The compiler also generates a matching conversion function which will convert the closure object to a pointer-to-function type. For example:

auto p = [](int n, int m) { return n < m; };
bool (*fp)(int, int) = p;
const bool n = fp(3, 4);

// n = true

A mutable lambda expression allows the lambda to internally modify a capture-by-copy variable. For example:

int n = 2;
[=]() mutable { n++; std::cout << n << "\n"; }();
std::cout << n << "\n";

// output:
3
2

Leaving the mutable specifier out in such use-case is a compiler error:

error: cannot assign to a variable captured by copy in a non-mutable lambda

A generalized lambda expression uses auto as its parameter type, and (generally) type deduction for its return type. Why is this useful? Consider the following lambda for computing the power of a number:

auto p = [](int n) { return n * n; };
const int n = p(5);

// n = 25

Pretty neat. However, what happens if we call p() with a argument of 2.25?

const auto n = p(2.25);

error: implicit conversion from 'double' to 'int' changes value from 2.25 to 2

Aha. We get a compiler error!1 The compiler has to do an implicit type conversion, which, if we were less stringent with our compiler flags, would’ve given us a value we probably didn’t expect. In other words we have a lambda that works on int’s only. A generalized lambda solves this:

auto p = [](auto n) { return n * n; };
const auto i = p(5);
const auto d = p(2.25);

// i = 25
// d = 5.0625

A generic lambda is also represented as compiler generated closure class, with the difference that its operator() it templated. For the above example the compiler will generate something like:

class __lambda0 {
public:
    template <typename T>
    T operator()(T n) const { return n * n; }
};

In a related fashion you can also do generalized lambda captures. They allow you to initialise a variable for use inside the lambda expression scope:

const int a = 1;
const int b = 2;
auto g = [n = a + b] { return n + n; }();

// g = 6

Why is this useful? Consider the use of objects that doesn’t support value semantics, say a std::unique_ptr. Those objects can’t be copied, they need to be moved, and with lambda init-capture like the one above you can accomplish that:

class Foo {
public:
    void SomeFunction() { std::cout << "Foo::SomeFunction()" << "\n"; }
};

std::unique_ptr<Foo> foo = std::make_unique<Foo>();
[f = std::move(foo)] { f->SomeFunction(); }();

// output:
Foo::SomeFunction()

Had we just tried to init-capture that with a normal copy assignment we’d have another compiler error:

error: call to implicitly-deleted copy constructor of
      'std::__1::unique_ptr<Foo, std::__1::default_delete<Foo> >'

One curious detail about init-captures is that the operands on either side of the assignment exists in different contexts2. What this means is that variable on the left hand side of the assignment is in the scope of the lambda, while the expression on the right hand side is in the enclosing scope of the lambda expression.

Usage

By now we have a pretty good understand of how a lambda expression is constructed and how it’s represented by the compiler. One question remains though; what do you actually use them for?

Use a lambda when you need a small effecient function object.

One of the most common use-case for lambdas is with STL and its various algorithms. The prototypical example of lambda use is that of sorting a STL container with a custom comparator. Let’s first consider how we might accomplish that without lambdas:

std::array<int, 5> n { 8, 3, 6, 1, 4 };

struct LessThanCmp {
    bool operator()(int a, int b) { return a < b; }
};

LessThanCmp myComparator;

std::sort(std::begin(n), std::end(n), myComparator);

That’s a fair amount of typing3. With lambdas we let the compiler do all of the boilerplate code for us, letting us focus on our intent:

std::array<int, 5> n { 8, 3, 6, 1, 4 };

std::sort(std::begin(n), std::end(n), [](int a, int b) { return a < b; });

Consider the example of having a container of (many) integers that we wish to perform various computations on. Performing these computations takes a long time so we’d like to do them asynchronously with the main thread. Lambdas provide a very conventient way of accomplishing that:

std::vector<int> v { 5, 4, 8, 2, 5 };

auto a = std::async([&v]{
         int r = 0;
         for (const auto& n : v) { r += n; }
         return r; });

auto b = std::async([&v]{
         int r = 1;
         for (const auto& n : v) { r *= n; }
         return r; });

std::cout << a.get() << "\n";
std::cout << b.get() << "\n";

// output:
24
1600

Imagine that we have lots of small computations like these that we want to perform only a few times, or sometimes never, during the lifetime of the application; then having to define and maintain named functions would’ve been wasteful and unclean.

Passing lambdas between functions are pretty straightforward, return auto to return from a function4 and use either a template parameter or std::function to pass into a function.

auto SomeFunction()
{
    return []{ std::cout << "returned from SomeFunction()\n";
}

auto f = SomeFunction();
f();

// output:
returned from SomeFunction()
void SomeFunction(std::function<void()> fn)
{
    fn();
}

SomeFunction([] { std::cout << "from main\n"; };

// outpu:
from main
template <typename T>
void SomeFunction(T fn)
{
    fn();
}

SomeFunction([] { std::cout << "from main\n"; };

// output:
from main

When passing lambdas around it’s important to beware of capturing local variables by reference (and sometimes by value) as that has undefined behavior when they go out of scope. Consider this somewhat contrived example:

auto Problem()
{
    char b[] = "local";
    return [&b]{ return b; };
}

auto p = Problem();
std::cout << p() << "\n";

// output:
<undefined, may crash or print garbage>

Summary

While everything a lambda does can be accomlished using other means, they do provide a nice shorthand for implementing callable objects. It can be argued that they sometimes obfuscate code but I believe that’s largely due to being unfamiliar with their syntax. With that said, lambdas are best used as short, few lines of code, function objects.

  • Use a lambda when you want a small local callable object.
  • A lambda doesn’t introduce a new name that’s for temporary use only.
  • Allows binding of variable outside its immediate scope.
  • Let’s you focus on intent without writing lots of boilerplate code.
  • They are beautiful.

What do you use lambdas for? Please share your thoughts and experiences in the comments below.

Thanks for reading!


  1. Always compile with all warnings enabled and treat them as errors. See the Embracing Compiler Errors For Fun And Profit post for more details. [return]
  2. ยง5.1.2 / 11.2 of the standard says that the second variable must bind to a declaration in the surrounding context. [return]
  3. It can be made shorter but less readable. [return]
  4. Can of course return std::function as well. [return]


comments powered by Disqus