Getting to know lambdas in C++
Getting to know lambdas in C++#
\(\lambda\)s in C++: Nameless functions serving sweet syntactic sugar — just enough to give your codebase diabetes.
Lambda Expressions#
Lambdas Reduce Boilerplate#
1class Plus {
2 int value;
3public:
4 Plus(int v) : value(v) {}
5
6 int operator()(int x) const {
7 return x + value;
8 }
9};
10
11auto plus = Plus(1);
12assert(plus(42) == 43);
13// turns into
14auto plus = [value = 1](int x) { return x + value; };
15
16assert(plus(42) == 43);
17// Reference: Back to Basics: Lambdas from Scratch - Arthur O'Dwyer - CppCon 2019
Lambdas Without Captures#
Lambdas have 2 types of parameters:
Parameters for behaviour (The capture group)
Parameters when they are called
Generic Lambdas (C++14) may call arguments of generic type. (
auto
,const auto&
)What the actual fcuk is this piece of code
1void f(int, const int (&)[2] = {}) {} // #1
2void f(const int&, const int (&)[1]) {} // #2
3// see entire code block at: https://en.cppreference.com/w/cpp/language/lambda
Lambdas are function object.#
have a “unique, unnamed non-union class type” – closure type
Examples:
1Example: 1
2auto add = [](int x, int y) -> int {
3 return x + y;
4}
5// has effect of
6class lambda??? { // closure type, compiler decides the name
7 public:
8 lambda???(); // only callable by the compiler before C++20
9 int operator() (int x, int y) const {
10 return x + y;
11 }
12}
13auto add = lambda???();
14// example taken from Back to Basics: Lambdas - Nicolai Josuttis - CppCon 2021
1// Example: 2
2while(...) {
3 int min, max;
4 ...
5 p = std::find_if(col1.begin(), col1.end(),
6 [min, max](int i) {
7 return min <= i <= max;
8 });
9}
10// has effect of
11class lambda??? {
12 private:
13 int min_, max_;
14 public:
15 lambda???(int min, int max) // only callable by the compiler
16 : _min(min), _max(max) {
17 }
18 int operator() (int x, int y) const {
19 return x + y;
20 }
21}
22while(...) {
23 int min, max;
24 ...
25 p = std::find_if(col1.begin(), col1.end(),
26 lambda???{min, max});
27}
28// example taken from Back to Basics: Lambdas - Nicolai Josuttis - CppCon 2021
1// Example: 3
2auto plus = [] (auto x, auto y) {
3 return x + y;
4}
5
6// Usage
7int i = 42;
8double d = plus(7.7,i);
9
10std::string s{"Hi"};
11std::cout << plus("s: ", s);
12
13// manually would look like this (but no need to do it this way)
14plus.operator()<double, int>(7.7, i);
15
16
17// has effect of
18class lambda??? {
19 public:
20 lambda???(); // only callable bu the compiler before C++20
21 template<typename T1, typename T2>
22 auto operator() (T1 x, T2 y) const {
23 return x + y;
24 }
25}
26auto plus = lambda???();
27// example taken from Back to Basics: Lambdas - Nicolai Josuttis - CppCon 2021
Generic lambda (function object) is different from the templated (generic) function.#
1// Function object with generic `operator()` method
2auto printLmbd = [](auto& col1) {
3 for (const auto& elem: col1) {
4 std::cout << elem << '\n';
5 }
6 };
7
8// Function template (generic before the call)
9template<typename T>
10void printFunc(const T& col1) {
11 for (const auto& elem: col1) {
12 std::cout << elem << '\n';
13 }
14}
15
16// usage
17std::vector<int> v;
18...
19printFunc(v);
20printLmbd(v);
21printFunc<std::string>("hello"); // OK
22printLmbd<std::string>("hello"); // Error
23
24call(printFunc, v); // Error
25call(printFunc<decltype(v)>, v); // OK
26call(printLmbd, v); // OK
Initializer in lambda capture (since C++14)#
1auto price = [disc = getDiscount(cust)] (auto item) {
2 return getPrice(item) * disc;
3}
Lambdas are stateless by default – Not allowed to modify local copies from captured by value#
mutable
makes them stateful (modification allowed)
1auto changed = [prev = 0] (auto val) {
2 bool changed = prev!=val;
3 prev = val; // Error: prev is read-only copy
4 return changed;
5}
1auto changed = [prev = 0] (auto val) mutable {
2 bool changed = prev!=val;
3 prev = val; // OK due to mutable
4 return changed;
5}
6std::vector<int> col1{7, 42, 42, 0, 3, 3, 7};
7std::copy_if(col1.begin(), col1.end(),
8 std::ostream_iterator<int>{std::cout, ""},
9 changed);
10// Output: 7, 42, 0, 3, 7
11
12std::copy_if(col1.begin(), col1.end(),
13 std::ostream_iterator<int>{std::cout, ""},
14 changed);
15// calling it again will not cause the 7 to removed.
16// standard algorithms takes callables by value,
17// so next call is operated over a copy of changed
18// Ref: Back to Basics: Lambdas - Nicolai Josuttis - CppCon 2021
Per-lambda Mutable State (Wrong Approach)#
1auto counter = []() { static int i; return ++i; };
2// This closure behaves like following class type:
3class Counter {
4 // No captured data members
5public:
6 int operator()() const {
7 static int i;
8 return ++i;
9 }
10};
11// example from Back to Basics: Lambdas from Scratch - Arthur O'Dwyer - CppCon 2019
There is just one static variable i
, shared by all callers of Counter::operator()()
!
Lambda capture behaviour
[=]
vs[g=g]
1int g = 10;
2
3auto kitten = [=]() { return g + 1; }; // Implicit capture by value
4auto cat = [g = g]() { return g + 1; }; // Explicit capture by value (copy of g)
5
6int main() {
7 g = 20;
8
9 printf("%d %d\n", kitten(), cat()); // Output: 21 11
10}
11// example from Back to Basics: Lambdas from Scratch - Arthur O'Dwyer - CppCon 2019
Never use
[=]
i.e. capture all by value. You might accidentally capture a vector of strings!!Also see slides 18…24 of Back to Basics: Lambdas from Scratch - Arthur O’Dwyer - CppCon 2019 on different ways of capturing a variable
Variadic Lambdas reduce boilerplate#
1class Plus {
2 int value;
3public:
4 Plus(int v);
5
6 template<class... As>
7 auto operator()(As... as) {
8 return sum(as..., value);
9 }
10};
11
12auto plus = Plus(1);
13assert(plus(42, 3.14, 1) == 47.14);
14
15// turns into
16
17auto plus = [value = 1](auto... as) {
18 return sum(as..., value);
19};
20
21assert(plus(42, 3.14, 1) == 47.14);
22// Reference: Back to Basics: Lambdas from Scratch - Arthur O'Dwyer - CppCon 2019
Comments