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 Without Captures.png

  • Lambdas have 2 types of parameters:

    1. Parameters for behaviour (The capture group)

    2. 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

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

References#

Comments