C++ Lambdas aren’t magic, part 2 🎆

There’s a lot of myths and odd-questions around lambdas. This guide will give you a deeper understanding of their rules and how they work.

Before reading, please read the part 1 of this series, otherwise some of the answers may not make sense.

Where part 1 explained what a lambda actually is under the hood, this will take more of an FAQ format and cover more in-depth questions. At the end of many questions, I will leave a link to further reading for the topic discussed.

At a glance

  • Lambdas are cheap
  • They allocate on the stack, not heap
  • Their assembly output is identical to using a class
  • You don’t need to capture static variables
  • If you want to return a lambda, use std::function or similar
  • Captured variables are immutable by default
  • Copying a lambda will copy its state
  • Capturing this is not a special case
A collection of people dressed in wizard garb, holding a sign saying 'We demand to be taken seriously.' Image from Arrested Development.
Lambdas can feel like dark magic if we don’t know what’s going on under the hood.

Here’s what we’ll cover

  • A quick recap on lambdas 🎓
  • Key points for deep understanding of lambdas 🗝️
  • Performance concerns 🚀
  • What are the variable capture rules? 🕸️
  • How to pass a lambda around 📧
  • Other miscellaneous points 🌴

Quick recap 🎓

As we explored in part 1, a lambda is an expression, which when evaluated, creates a local functor class with operator() ready to call. It may also capture variables by passing them through the functor’s constructor.

A note on terminology:

Technically, a lambda is an expression that defines a functor class. A lambda is to a class, as a function is to an instance of a class.

For simplicity, I will use the term “lambda” to refer generally to the concept.

Highlighting the various parts of a lambda.

Key points 🗝️

What is the type of a lambda?

The type of a lambda is determined at compile time. You’re essentially creating an anonymous class, and if you inspect your assembler output you’ll see it has a name as such:

lambda_4a776e7774c8d4ec8eddd924a4a3b251

As such, each lambda creates its own unique type.

What assembler code is generated by a lambda?

Whether or not you capture variables, it’s the same assembler code as for a normal class. The only exception is that if capturing variables, the constructor is inlined as it won’t be re-used anywhere else.

Please read the following article for a deeper dive and proof of this:

Why can’t I assign one lambda to another?

Because the types are different. Consider writing two separate classes and assigning them to each other — it’s not valid C++.

Performance concerns 🚀

How expensive is a lambda?

It’s cheap. (Depending on your definition of cheap!)

Memory wise: As a lambda will generate a class, it will be as expensive as creating an equivalent class that holds the same number of variables as you capture. Simply put, the more variables you capture (particularly by value), the larger your generated functor class, and the more expensive your use of a lambda will be. If you capture by reference generally, this will be no more than a few pointers.

Computationally: If you don’t capture any variables, it is literally a function call.You can’t really get any cheaper!

If you do capture a variable, the cost is the same as constructing an object and calling a function directly on it, with no virtual look-ups. (Also cheap.)

Importantly: The cost of a lambda is never greater than the cost of an equivalent function / class.

Does using a lambda allocate on the heap?

No. The functor instance will be created on the stack as if you constructed a class directly. However, putting std::function on the left instead of auto may heap allocate. See:

More on std::function later.

Capturing variables 🕸️

The key thing to know here is that only automatic variables can be captured. This is any local variable to your function, including the this pointer. These are pushed on and off the stack, and automatically destructed when you leave the current function.

Do [=] and [&] capture all surrounding variables?

No, this is a common misconception!

They will capture every variable that is used in the function definition, and no more. Your coding style may dictate you should explicitly name each captured variable however.

Can I have default capture variable values?

Yes, but only since C++14.

What’s so special about capturing this?

Nothing at all. this is a pointer to the current object, and is captured explicitly or implicitly like any other variable. Remember that when writing class code, you can access member variables without writing this->. This omission is the cause of some confusion with lambdas in classes.

In both of the following cases, you are capturing a pointer by value, and then dereferencing it.

Why can’t I capture a static reference?

You can in C++14 and higher. However, a lambda can only capture automatic variables, and a static variable, by definition, is not automatic.

You don’t need to capture static variables as they always have the same address in memory. It isn’t required — just use the static variable directly.

Note, some compilers may have static-capturing support before C++14 and warn as such, but the point still stands that you do not need to capture them.

Why can’t I capture class member variables?

You can, but recall that you can only capture automatic variables. This means you cannot explicitly capture member variables, because they’re actually behind a dereferenced pointer.

To access member variables, you need to capture this, whether explicitly or implicitly.

I want to capture some variables by value, and others by reference.

This is perfectly valid, use the following:

Passing lambdas around 📧

How can I return a lambda from a function?

If you’re using C++14 and above, you can set a function’s return type to be auto. This will let you use the functor type directly.

In C++11 and below, you cannot return auto from a function, which poses a problem for the lambda type. The simplest way is to avoid this is convert it using std::function, or some library alternative that can wrap the function nicely for you.

When using std::function, watch out for potential heap allocations if you capture many variables, and the overhead of calling a function through a pointer rather than directly. For small or empty capture lists, the memory footprint of std::function will be the same as the actual functor type due to a small object optimization.

Please read the following for more info how later versions of C++ will allow returning by auto:

And a comparison of an auto lambda type vs one wrapped in std::function:

It is possible to return a lambda by function pointer too, however the syntax is awful, and you will likely run into dangling allocation and lifetime issues. I strongly recommend against this unless you have a very specific use-case that either auto or std::function cannot fulfill.

What happens if I copy a lambda?

You will copy the state as is, like copying a class object directly (assuming it has not overridden the copy constructor.)

This can be hard to visualise and catches out many people, so I encourage you run the following code and play around with it yourself:

Other miscellaneous points 🌴

Can I nest lambdas?

Sure, knock yourself out. Lambdas can even return other lambdas if required:

Why can’t I mutate captured variables, even those I’ve copied?

Let’s take a look at the example generated code from part 1.

Note how on line 3, the operator() declaration is const, meaning it cannot mutate variables on the class.

If you need this to be non const, you can use the mutable keyword, as so:

What type does a lambda function return when called?

So far, we’ve just written a lambda and let the compiler figure out what type it will return, as if by magic.

Without explicitly specifying, lambdas will use “auto return type deduction rules”: essentially the same as if you put auto to the left of a variable.

Can I define an explicit return type?

Yes, using alternative function syntax. Here, the return type is on the right hand side of the function rather than the left. You can use this in regular functions, member functions on classes, and lambdas too.

To conclude

I’ve covered what I see as regular confusion points surrounding lambdas here. If you’ve learnt anything from this, or have further questions, please leave a comment or shoot me a message on Twitter @winwardo and I’ll update this guide :)