Lambdas

An introduction to lambda expressions - a concise way of defining simple, ad-hoc functions
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

In this lesson, we introduce Lambdas. They provide a more concise syntax for creating functions. Lambdas give us more options for designing our software, as they have two key differences compared to regular functions:

  • They can be defined almost anywhere - including within the bodies of other functions
  • They can be anonymous - we don't need to give them a name

This makes lambdas the default choice for scenarios needing simple, one-off functions. For example, if we need a function to pass as the parameter of another function, lambdas are often the default choice.

This is because they make our code faster to write, and more readable. The lambda is defined at the same place it is being used and, if the lambda is used only in that place, we don't need to clutter our code with unnecessary identifiers.

Lambda Expressions

A minimalist lambda expression uses a set of square brackets, [], followed by a code block, {}. Between the { and }, we implement the behaviors our lambda will perform.

In this example, we create a lambda that does some logging:

[]{
  std::cout << "Hello from Lambda!";
};

By itself, the expression doesn't do anything. However, we can treat it like any other expression. For example, we can immediately call the lambda using ():

#include <iostream>

int main() {
  []{
    std::cout << "Hello from Lambda!";
  }();
}
Hello from Lambda!

We can store the lambda as a variable, and can then call it through that variable later:

#include <iostream>

int main() {
  auto MyLambda { []{
    std::cout << "Hello from Lambda!";
  }};

  MyLambda();
}
Hello from Lambda!

However, by far the most common use of a lambda is to pass it to another function. Below, our CallIfEven() function accepts an integer and a callback. If the integer is even, the callback will be invoked.

In previous lessons, we saw how we could provide such a callback using a function pointer or a functor. But, these constructs require us to define a function or a type elsewhere in our code.

If we just need a simple callable to implement some behavior, using a lambda is the best choice:

#include <iostream>

void CallIfEven(int Num, auto Callback) {
  if (Num % 2 == 0) {
    Callback();
  }
}

int main() {
  CallIfEven(4, [] {
    std::cout << "4 is even";
  });
}
4 is even

Lambda Types

Here, we’re always using auto to let the compiler figure out the lambda type. Explicitly declaring the type of a lambda is quite tricky, and generally not something that is done.

We will introduce better ways of specifying function types in the next lesson.

Using Parameters

To allow our lambda to accept parameters, we can introduce a set of parenthesis, ( and ) to the basic lambda structure.

Below, we provide an alternative way to define a lambda:

[](){
  std::cout << "Hello from Lambda!";
};

This lambda still accepts no parameters, but we can now provide a parameter list within the ( and ). This parameter list works like any other function.

Below, we define a lambda that accepts two int arguments:

[](int x, int y){
  std::cout << "The total is " << x + y;
};

We have access to all the same techniques as functions, including passing by reference and using const:

[](const int& x, const int& y){
  std::cout << "The total is " << x+y;
};

We also pass arguments into those parameters in the usual way, within the ( and ) when we call our lambda:

#include <iostream>

int main() {
  auto MyLambda { [](int x, int y){
    std::cout << "The total is " << x + y;
  }};

  MyLambda(2, 5);
}
The total is 7

Specifying Return Types

By default, the compiler infers the return type of the lambda. This is equivalent to having a function with an auto return type:

// This will return a boolean
[](int Number){
  return Number % 2 == 0;
};

We may want to explicitly specify the return type of our lambda. If we wanted to explicitly set the return type to be a bool, the syntax looks like the following:

[](int Number) -> bool {
  return Number % 2 == 0;
};

This has the same effect as specifying a return type on a regular function. Specifically, if we attempt to return something that does not have that type, the compiler will attempt to implicitly convert it.

For example, the body of the following lambda attempts to return an int, but because we’ve specified the lambda’s return type as bool, our return value will be converted to a boolean. In this case, our lambda will return true:

[](int Number) -> bool {
  return Number % 2 == 0;
};

When specifying a return type, we must include a parameter list using ( and ), even if it is empty:

[]() -> bool {
  return true;
};

Capturing Variables

Normally, we can access values from the parent scope. However, this does not work with lambdas. The following code will not compile:

#include <iostream>

int main() {
  int Number{2};

  [] {
    std::cout << "Number: " << Number;
  };
}
error C3493: 'Number' cannot be implicitly captured because no default capture mode has been specified
error C2326: lambda function cannot access 'Number'

To give our lambda access to variables within the scope it is defined, we need to capture them.

The [] in a lambda expression is where we capture variables from the parent scope we want to use:

#include <iostream>

int main() {
  int Number{2};

  [Number] {
    std::cout << "Number: " << Number;  
  };
}
Number: 2

We can capture multiple objects by separating them with commas:

#include <iostream>

int main() {
  int x{1};
  int y{2};
  int z{3};

  [x, y, z] { 
    std::cout << "Sum: " << x + y + z;
  }();
}
Sum: 6

Modifying Captures

We can create new variables in the capture clause. This is useful mainly for performing subtle modifications to what we’re capturing. For example, we can give our captures different names, or cast them:

#include <iostream>

int main() {
  int x{1};

  [Value = x, Casted = static_cast<bool>(x)] {  
    std::cout << "Value: " << Value << '\n';
    std::cout << "Casted type: "
      << typeid(Casted).name();
  }();
}
Value: 1
Casted type: bool

mutable Lambdas

By default, lambdas capture objects by const value. This means that the objects we capture are copied into our lambda, and we are then prevented from modifying them:

int main() {
  int Number{2};

  [Number] {
    ++Number; 
  };
}
error C3491: a by copy capture cannot be modified in a non-mutable lambda

To allow modification of these copies, we can mark our lambda as mutable. Applying the mutable keyword requires us to include the parameter list syntax (), even if it is empty:

#include <iostream>

int main() {
  int Number{2};

  [Number]() mutable {  
    std::cout << "Number in Lambda: "
      << ++Number;
  }();
}
Number in Lambda: 3

Why do we need mutable?

It might seem strange that lambdas capture values by const value, but this is due to a nuance in how lambdas work. Behind the scenes, lambdas work similarly to functors, with the captured objects becoming member variables on that type.

This has implications when our lambda is called multiple times. If a lambda invocation modifies one of those data members, the outcome of future lambda calls can be affected:

#include <iostream>

int main() {
  int Number{2};

  auto Lambda{[Number]() mutable {
    std::cout << "Number in Lambda: "
      << ++Number << '\n';
  }};

  Lambda();
  Lambda();
  Lambda();
  std::cout << "Number in main: " << Number;
}
Number in Lambda: 3
Number in Lambda: 4
Number in Lambda: 5
Number in main: 2

This is rarely desirable, so the specification has decided that if we do want it, we have to take the extra step of marking the lambda mutable. This ensures the behavior is intentional and clearly communicated.

Capturing by Reference

Similar to parameters, we can elect to capture objects by reference instead of by value. We do this by prepending & to the identifiers:

#include <iostream>

int main() {
  int Number{2};

  [&Number] {  
    ++Number;
  }();

  std::cout << "Number: " << Number;
}
Number: 3

Capturing by const Reference

When we want to capture an object by reference, and also mark that reference as const, the std::as_const() function within the <utility> header can help us:

#include <utility>

int main() {
  int x{1};

  [&x = std::as_const(x)] {
    x++;  
  };
}
error: increment of read-only reference 'x'

Default Captures

For simpler use cases, the compiler can help us create our capture list. It knows what variables we’re using in our lambda, so it can automatically capture them for us.

We can ask it to capture everything we use by value, using the = symbol in our capture group:

#include <iostream>

int main () {
  int x { 1 };

  [=] {
    std::cout << "x: " << x;
  }();
}
x: 1

To capture everything by reference, we can use &:

#include <iostream>

int main () {
  int x { 1 };

  [&] { ++x; }();

  std::cout << "x: " << x;
}
x: 2

We can mix and match these techniques as needed. If we’re using a default capture, it needs to be first:

#include <iostream>

int main() {
  int x{1};
  int y{2};
  int z{3};

  // Capture x by reference
  // Capture y by const reference
  // Capture everything else by value
  [=, &x, &y = std::as_const(y)] {
    std::cout << "Sum: " << x + y + z;
  }();
}
Sum: 6

Using Attributes

Lambdas can specify attributes such as [[nodiscard]] after the capture group:

int main() {
  auto Lambda{[] [[nodiscard]] { return 5; }};

  Lambda();
}
warning: ignoring return value of 'Lambda', declared with attribute 'nodiscard'

If our lambda includes a parameter list, we place the attribute before it. Below, we use the [[deprecated]] attribute in a lambda that has both a parameter list and a return type:

int main() {
  auto Lambda{[] [[deprecated("Oh no!")]]
      (int x, int y) -> int { return x + y; }
  };

  Lambda(1, 2);
}
warning: Lambda is deprecated: Oh no!

Summary

In this lesson, we've explored lambdas, demonstrating how they provide a concise way to write inline functions for specific tasks. The key topics we learned includes:

  • Lambdas offer a concise syntax for creating anonymous functions that can be defined almost anywhere, including within other functions.
  • A basic lambda expression consists of a set of square brackets for captures, followed by a code block where the lambda's functionality is implemented.
  • Parameters can be added to a lambda by including them in parentheses between the capture brackets and the code block.
  • The return type of a lambda can be automatically inferred by the compiler, or explicitly specified using the -> syntax followed by the type.
  • Captures allow a lambda to access variables from its enclosing scope, either by value or by reference, with the ability to modify captured variables if the lambda is marked as mutable.
  • Default captures can simplify code by automatically capturing all used variables either by value or by reference, as indicated by = or &, respectively.
  • Lambdas can also specify attributes like [[nodiscard]] or [[deprecated]] to enforce certain behaviors or warnings at compile time.

Was this lesson useful?

Next Lesson

Standard Library Function Helpers

A comprehensive overview of function helpers in the standard library, including std::invocable, std::predicate and std::function.
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 125 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Standard Library Function Helpers

A comprehensive overview of function helpers in the standard library, including std::invocable, std::predicate and std::function.
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved