C++ Lambda Anonymous Functions - The Simple Guide

By Steve Claridge on 2022-12-14.

Here I will try and explain what lambda functions are and how you can use them in your C++ code.

So what are we talking about anyway?

There's a lot of different names for this: anonymous function, lambda function, first-class function, function literal, lambda expression or lambda abstraction.

I think anonymous function makes the most sense to describe this as it is a function without a name. Let's dive straight into an example so you will see how it doesn't have a name. From now on I'm going to use the term anonymous function throughout, instead of lambda or any other name.

When a function is a variable

First of all, here's a piece of code (with no lambdas) that simply defines an int variable a and passes it to function stuff(), which prints it out. Bread and butter stuff, very simple:

void stuff(int num) 
{
  cout << num << endl;
}

int a = 10;
stuff(a);

The point on show here is that we've created a variable and passed it to a function.

One of the things you can do with anonymous functions is treat them like a variable, so instead of passing an int to stuff() we can pass a function to it!

void stuff(std::function<void()> func) 
{
  cout << func() << endl;
}

auto a = [](){cout << "hello" << endl;};
stuff(a);

Yeah, shit got real very quickly with the syntax, don't worry about it yet, all you need to know now is that we can create a function without a name and pass it around like it's an int or any other type of variable.

Already we've got problems

Normally when you create a function in C++, it has a defined scope. For example, if you create a class and add some public/private functions then those functions are scoped to that class. If you create a function in a file called stuff.cpp then it is scoped to that cpp file.

It's a bit more complicated with anonymous functions because they can move around, so their scope changes. We saw in the example about that I created an anonymous function, assigned it to variable a and then passed it to another function called stuff(), where it was executed. That means, when I created the function, it was scoped differently to when I ran it. Lets see another example to explain this:

void stuff(std::function<void()> func) 
{
  func();
}

string name = "Bobby Dazzler";

auto a = [](){cout << name << endl;};
stuff(a);

Again, don't worry about syntax yet, just working through some concepts about scope. So, now we have a new name variable and our anonymous function [](){cout << name << endl;} prints it to standard out. Don't forget though that the anonymous function is passed to stuff() and then executed there.

Notice that at the point that I define the anonymous function, the variable name is available, but what about when we execute the anonymous function inside of stuff()? How is name available then? Clue: it's not.

About that syntax

Lets break down some of that funky-looking syntax, here's a definition of an anonymous function:

[](){}

Yeah, I know. Here's a traditional function:

int more_stuff(int a, int b)
{
  //stuff happens here
}

Our more_stuff function has a return type of int and accepts two parameters. Our anonymous function needs some parameters too, lets specify two ints again:

[](int a, int b){}

So the () part of the definition is the same as in a traditional function definition, it contains the parameters. The {} part of the definition is also the same as before, it holds the code, we can, for example add them together:

[](int a, int b){return a + b;}

What about the return type?

I'm glad you asked.

The C++ compiler can automatically deduce the return type from the code, in the above example the return type of the anonymous function is implicitly an int. Similarly, if we had this function then the return type would automatically be a string:

[](string a, string b){return a + b;}

You can specify the return type if you want to, it adds some more funk onto that already funky syntax. :

[](int a, int b) -> {}

Using the same example as above, but this time we want to return a long in case the two input ints are too large to return as an int:

[](int a, int b) -> long {return a + b;}

And the [] ?

Think back to the section at the start about scoping variables. As I said above, an anonymous function can be passed around like a variable, which means that it could be executed in any function it is passed to. This means that you can't safely assume what variables are available (in scope) when the function is executed. Lets see an example where this will produce an error:

void run_it(std::function<int()> fnc) {

    std::cout << fnc() << std::endl;
}

int main () {

    int c = 7;

    auto a = []() -> int {return (10 * 4) * c;};

    run_it(a);
}

if you compile this, g++ will complain with variable 'c' cannot be implicitly captured in a lambda. The anonymous function tries to use the variable c, this might look to be OK because c is declared directly above the anonmyous function declaration. The important thing to remember here is that auto a = []() -> int {return (10 * 4) * c;}; is declaring a function that will be run later, not actually running the function. So, yeah c is in scope at the time of declaration, but it won't be when the function is run inside of run_it. The compile realises this and complains, so how do we fix it?

the [] part of the anonymous function definition is the capture part, it's where you can specify variables that you want to remain in scope when the function is run but they aren't actual parameters of the function. Lets change the above code by adding c as a captured variable:

    int c = 7;
    auto a = [c]() -> int {return (10 * 4) * c;};
}

This will now compile, you can put pretty much anything inside the capture, if your function is being defined within a class you can capture this for example.

About std::function

We've been creating functions that accept a function as a parameter, e.g.:

void run_it(std::function<void()> func) 
void run_it2(std::function<int(string, string)> func) 

The syntax here simply defines the return type and any parameters of a function that it will accept within <>. So std::function<void()> returns a void and has no parameters. function<string(int, char)> returns a string and has an int and char parameter.

A working example

#include <iostream>
#include <functional>

void run_it(std::function<int(int, int)> fnc) {

    std::cout << fnc(4, 3) << std::endl;
}

int main () {

    int c = 4;
    auto a_func = [c](int a, int b) -> int {return (a * b) * c;};
    run_it(a_func);

    c = 2;
    auto another_func = [c](int a, int b) -> int {return ((a - b) * 100) / c;};
    run_it(another_func);
}

run_it accepts a function that returns an int and has two int parameters. Two anonymous functions are created a_func and another_func, both capture c, if you compile and run this it will print out

48
50

You can compile this with g++ -std=c++11 test.cpp

Some things to try

A few things to try to really nail your understanding:

  1. Create a run_it2 function that accepts a function that has two string parameters and call it with two newly defined anonymous functions.
  2. Move the run_it calls to the bottom of main and observe if the value of c changes in each call.
  3. Create run_it3 that accepts a function that has no parameters and returns a void. Call it with an anonymous func.