Skip to content

Instantly share code, notes, and snippets.

@Eczbek
Last active September 12, 2025 02:14
Show Gist options
  • Select an option

  • Save Eczbek/59aca2bd6cf2250df312afd2db436d44 to your computer and use it in GitHub Desktop.

Select an option

Save Eczbek/59aca2bd6cf2250df312afd2db436d44 to your computer and use it in GitHub Desktop.
article on lambdas

Lambdas

A lambda expression is a shorthand notation for creating an unnamed callable object (also called a closure, or an anonymous function). A lambda can "capture" variables from its surrounding scope by value or by reference, allowing the body of the lambda to access or modify those variables without having to pass them as parameters. Unlike regular functions, lambdas are typically written in-line, combining the reusability of functions with direct access to local context. The lambda retains captured variables' state (for captures by value) or dynamically references them (for captures by reference), making lambdas ideal for short, context-dependent operations like custom comparisons, filters, or event handlers.

Example:

bool is_even(int x) {
	return x % 2 == 0;
}

int main() {
	std::vector<int> data = { 1, 3, 5, 10, 73 };

	bool has_even = std::ranges::any_of(data, is_even);
}

The above code can be rewritten as:

int main() {
	std::vector<int> data = { 1, 3, 5, 10, 73 };

	bool has_even = std::ranges::any_of(data, [](int x) { return x % 2 == 0; });
}

Syntax

The basic syntax of a lambda expression looks like this:

[captures](parameters) -> return_type {
	statements;
}

A more detailed explanation of the syntax can be found on cppreference.

Capture list

All lambda expressions begin with a capture list. The capture list specifies which variables to capture from the surrounding scope:

int x = 10;

auto lambda = [x](int y) { return x + y; };

lambda(5) // 15
  • [] - Empty capture list.
  • [=] - Automatically capture variables that are used in the lambda body by value.
    • Mutually exclusive with [&].
    • Does not implicitly capture this.
  • [&] - Automatically capture variables that are used in the lambda body by reference.
    • Mutually exclusive with [=].
    • Implicitly captures this.
  • [x] - Capture only x by value.
  • [&x] - Capture only x by reference.
  • [this] - Capture *this by reference.
    • Capturing [&this] is invalid.
  • [x = y] - Define a local variable x and initialize it to y.
  • [x...] - Capture a pack x by value.
  • [&x...] - Capture a pack x by reference.

Different captures can be mixed together:

int a = 1;
int b = 2;
int c = 3;

[=, &b, d = c]() {
	// `a` is implicitly captured by value,
	// `b` is explicitly captured by reference, and
	// `d` is initialized to `c`.
	return a + b + d;
}

However, [=] may not be followed by other explicit captures by copy and [&] may not be followed by other explicit captures by reference.

  • Lambdas with implicit captures only capture what they actually use, so [&] and [=] can be used without fear of bloat or additional overhead.

Implicit captures can be nested multiple times:

int x = 0;

[&]() {
	[&]() {
		x = 5;
	}();
}();

x // 5

Parameters

If a lambda takes no parameters, the parameter list may be omitted entirely:

auto very_important_number = [] { return 4; };

Lambda parameters work the same way as normal function parameters, and auto parameters make a lambda's operator() implicitly templated.

The explicit this parameter can also be used with lambdas since C++23, allowing easy recursion:

auto fibonacci = [](this auto self, int n) {
	if (n < 2) return n;
	return self(n - 1) + self(n - 2);
};

Return type

A lambda's return type may be specified with an arrow:

auto add = [](int x, int y) -> int { return x + y; };

Omitting the return type is the same as writing -> auto.

Specifiers

  • constexpr - Explicitly specifies that a lambda's operator() is a constexpr function.
    • Mutually exclusive with consteval.
    • Lambdas are implicitly marked constexpr, if suitable.
  • consteval - Makes a lambda's operator() an immediate function.
    • Mutually exclusive with constexpr.
  • static - Makes a lambda's operator() a static member function.
    • Mutually exclusive with mutable.
    • Cannot be used if the captures list is not empty, or an explicit this parameter is present.
  • mutable - Allows the body of the lambda to modify the variables captured by value.
    • Mutually exclusive with static.
    • Cannot be used if an explicit this parameter is present.
auto next = [i = 0] mutable { return i++; };

next() // 0
next() // 1
next() // 2

These may be followed by a noexcept specifier, to determine whether calling the lambda may throw an exception.

Template parameters

A lambda's operator() may be templated to accept template parameters since C++20:

auto lambda = []<typename T>(const T& x) {
	return x;
};

// Deduce template argument from function argument
lambda(5);

// Pass template argument explicitly
lambda.template operator()<double>(5);

Attributes

Lambdas may be given attributes that apply to their operator()s since C++23:

auto very_important_number = [] [[nodiscard]] { return 4; };

For details, see cppreference.

Notes

Function pointers

Lambdas without captures can be converted to function pointers:

auto* ptr = +[] { return 4; };

This can be done with or without a static specifier on the lambda.

Unique types

Each lambda expression has its own unique, unnameable type:

auto add = [](int x, int y) { return x + y; };

It is similar to defining a new unnamed class for every lambda:

struct {
	auto operator()(int x, int y) const {
		return x + y;
	}
} add;

This also means that identical lambda expressions always have different types:

auto foo = [](int x) { return x + 1; };
auto bar = [](int x) { return x + 1; };

static_assert(not std::same_as<decltype(foo), decltype(bar)>);

Note that lambdas can not be converted to other lambda types.

Inheritance

Lambdas can be derived from:

auto base = [] { std::puts("Hello, world!"); };

struct : decltype(base) {} derived;

derived(); // prints "Hello, world!"

This, in combination with multiple inheritance, allows for a very neat "visitor" class:

template<typename... Fs>
struct visitor : Fs... {
	using Fs::operator()...;
};

visitor {
	[](int) { std::puts("int"); },
	[](double) { std::puts("double"); },
	[](...) { std::puts("other"); }
}(0); // prints "int"

Capturing function parameters

While you can use a function's parameters in its noexcept specifier and trailing requires clause, using a lambda there which captures the function's parameters is invalid:

// Fine
void f(int x) noexcept(noexcept(x)) {}

// Invalid
void f(int x) noexcept(noexcept([x] { x; })) {}

This is because the lambda is technically not in function or class scope, and thus cannot have captures (see expr.prim.lambda.capture#3).

Type aliases

Making a type alias containing a lambda expression in a header file or module interface violates the One-Definition Rule because aliases are not a "definable item", so lambdas in them are not allowed to match with other lambda declarations in other translation units:

// Bad
using T = decltype([] {});

// Bad
template<auto> struct A {};
using T = A<[] {}>;

// Ok
auto lambda = [] {};
using T = decltype(lambda);

See CWG2988 about lambda expressions in concept definitions.

In-line partial specialization

Usually, partial specialization requires declaring multiple structs in namespace scope:

using Function = int(char);

template<typename>
struct return_type;

template<typename Return, typename... Args>
struct return_type<Return(Args...)> {
	using type = Return;
};

static_assert(std::same_as<int, return_type<Function>::type>);

However, the same work can be done completely in-line with the help of an immediately-invoked lambda expression (IILE):

using Function = int(char);

static_assert(std::same_as<
	int,
	decltype([]<typename Return, typename... Args>(std::type_identity<Return(Args...)>) {
		return std::type_identity<Return>();
	}(std::type_identity<Function>()))::type
>);

Here, types are wrapped in std::type_identity objects to pass them around without actually constructing Function, Return(Args...), or Return.

IILEs are also useful in concepts:

template<typename>
struct type {};

template<typename T, template<typename...> typename Template>
concept specialization_of = requires {
	[]<typename... Args>(type<Template<Args...>>) {}(type<T>());
};

static_assert(specialization_of<std::vector<int>, std::vector>);

and in combination with std::integer_sequence:

[]<std::size_t... i>(std::index_sequence<i...>) {
	(..., std::print("{} ", i));
}(std::make_index_sequence<5>());
// prints "0 1 2 3 4"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment