What is a Lambda (Closure)
c++17
intermediate
Lambdas, also known as closures, are unnamed function objects in C++.
Unnamed means we cannot know or name their type
.
Because their type
is unknown to us lambdas can only ever be stored in an auto
variable on the stack.
Function means lambdas implement the function call operator(()
) and object means lambdas can store data, just like a struct or class.
Here’s a basic example:
#include <iostream> int main() { auto square = [](int n) { return n * n; }; int x = square(7); std::cout << x << "\n"; }
49
The Capture Group ([]
)
Most of the above lambda is self explanatory, everything after the “[]” is the function part of “function object”, but what about the object part? That’s where the capture group ([]
) comes in. By default, lambdas cannot access any variables outside their own scope or parameters at all. If we want to pass additional data to a lambda that isn’t a parameter we use the capture group ([]
). Capture groups capture their values by copy when the lambda is defined, but you can also pass values by reference(&
) to a capture group. Here’s an example:
#include <iostream> int main() { int offset = 0; auto square = [offset] (int n) { return (n * n) + offset; }; auto square2 = [&offset](int n) { return (n * n) + offset; }; int x = square(7); int y = square2(7); std::cout << x << ", " << y << "\n"; offset = -5; x = square(7); y = square2(7); std::cout << x << ", " << y << "\n"; }
49, 49 49, 44
There’s two special capture group defaults you can use, &
and =
.
&
means capture all used variables by reference.=
means capture all used variables by copy.
It’s not advised to use these two special capture groups as they can make your code less clear.
#include <iostream> int main() { int offset = 0; auto square = [=](int n) { return (n * n) + offset; }; auto square2 = [&](int n) { return (n * n) + offset; }; int x = square(7); int y = square2(7); std::cout << x << ", " << y << "\n"; offset = -5; x = square(7); y = square2(7); std::cout << x << ", " << y << "\n"; }
49, 49 49, 44
If the lambda is defined in a member function of an object, you can also capture this
to capture the entire object itself by reference. Here’s an (admittedly rather contrived) example:
#include <iostream> class NumberSquarer { public: int square(int n) { auto square = [this](int n) { return (n * n) + offset; }; return square(n); } void set_offset(int offset) { this->offset = offset; } private: int offset = 0; }; int main() { NumberSquarer ns; int x = ns.square(7); std::cout << x << "\n"; ns.set_offset(-5); x = ns.square(7); std::cout << x << "\n"; }
49 44
You can also rename and/or move values when capturing them if you’d like by declaring new variables right in the capture group:
#include <iostream> #include <string> int main() { std::string greeting{"Hello cppbyexample.com"}; auto sayCopy = [g = greeting]() { std::cout << g << "\n"; }; auto sayMove = [g = std::move(greeting)]() { std::cout << g << "\n"; }; sayCopy(); sayMove(); }
Hello cppbyexample.com Hello cppbyexample.com
Generic Lambas
If you don’t know the type of the parameters of your lambda you can specify them as auto
and C++ will deduce the type for you. Here’s an example:
#include <iostream> int main() { auto square = [](auto n) { return n * n; }; std::cout << square(7) << "\n"; std::cout << square(3.4f) << "\n"; }
49 11.56
Passing Lambdas to Functions
Because we can’t know the type
of a lambda we must use a template to pass lambdas directly to another function.
If you have explored the <algorithm>
library then you will see many algorithms that accept a template parameter called “Predicate” to conditionally perform some action. Here’s a minimal but explicit example of a “std::count_if
-like” algorithm:
#include <iostream> #include <vector> template <class Predicate> int count_if(const std::vector<int>& vec, Predicate p) { int count = 0; for (const int& n : vec) { if (p(n)) count++; } return count; } int main() { std::vector vec{1, 2, 3, 4, 5, 6, 7, 8, 9}; auto is_odd = [](int n) { return n % 2 != 0; }; int count = count_if(vec, is_odd); std::cout << count << "\n"; }
5
Storing Lambdas on the Heap
Earlier we said lambdas must always be declared auto
when declared on the stack, but what about storing lambdas for future use on the heap? The C++ standard library provides the std::function
object for just such a purpose. std::function
deserves its own article but when it comes to lambdas just know that std::function
can store and call them just fine.
#include <iostream> #include <functional> class MathOperation { public: MathOperation(std::function<int(int a, int b)> op) : op_(std::move(op)) { } int call(int a, int b) { return op_(a, b); } private: std::function<int(int a, int b)> op_; }; int main() { MathOperation add([](int a, int b) { return a + b; }); MathOperation mul([](int a, int b) { return a * b; }); int x = add.call(5, 2); int y = mul.call(x, 6); std::cout << x << ", " << y << "\n"; }
7, 42
Recursive Lambdas
Recursion is tricky with lambdas.
A lambda currently being defined cannot refer to itself in C++.
Practically this means you can’t use the name you intend to assign the lambda in the body or definition of the lambda itself (inside the curly braces [{}
]).
However, we can define a lambda as taking another lambda parameter with auto
.
If we then pass the lambda to itself we have created a recursive lambda.
An example will make things more clear:
#include <iostream> int main() { auto factorial = [](int i, auto& self) { if (i == 1) return 1; return i * self(i - 1, self); }; std::cout << factorial(10, factorial) << "\n"; }
3628800
Are Lambdas Efficient?
Yes! Lambdas in C++ are compiled as if you had written the pure function or the function object with the call operator(()
) yourself.
If you’re familiar with C you’d expect a lambda with no capture group to be equivalent to a function pointer and you’d be correct.
Here’s a “C” version of our count_if
example from earlier and we can still pass a lambda to it:
#include <iostream> #include <vector> extern "C" int count_if(int* vec, int len, bool (*p)(int)) { int count = 0; for (int i = 0; i < len; i++) { if (p(vec[i])) count++; } return count; } int main() { std::vector vec{1, 2, 3, 4, 5, 6, 7, 8, 9}; auto is_odd = [](int n) { return n % 2 != 0; }; int count = count_if(vec.data(), vec.size(), is_odd); std::cout << count << "\n"; }
5