This is my learning notes from the course - Mastering C++ Standard Library Features.
Mastering Lambdas
- Lambdas and the Standard Library
- Lambdas as local functions
- IIFE idiom
- Using higher order functions to build safer interfaces
Lambdas and the Standard Library
- Using lambdas with:
<algorithm>
<thread>
and<future>
<tuple>
Lambdas and Algorithms
The Standard Library provides dozens of algorithms that accept one or more
FunctionObject
parametersMost of them reside in the
<algorithm>
headerOthers, like
std::accumulate
, reside in<numeric>
std::for_each
Reference: https://en.cppreference.com/w/cpp/algorithm/for_each1
2
3
4
5std::vector<int> v{0, 1, 2, 3};
std::for_each(std::begin(v), std::end(v), [](int x)
{
std::cout << x;
});In C++17, you will be able to specify an execution policy:
std::execution::seq
std::execution::par
std::execution::par_unseq
1
2
3std::for_each(std::execution::par,
std::begin(v), std::end(v),
[](int x){ something(x); });
std::all_of
, std::any_of
, std::none_of
Reference: https://en.cppreference.com/w/cpp/algorithm/all_any_none_of
- Examples:
1
2
3
4
5
6
7struct person
{
std::string _name;
int _age;
};
std::vector<person> ps{ /* ... */ };1
2
3
4
5const bool can_drink = std::all_of(
std::begin(ps), std::end(ps), [](const auto& p)
{
return p._age >= 21;
});1
2
3
4
5
6const bool all_have_names = std::none_of(
std::execution::par_unseq,
std::begin(ps), std::end(ps), [](const auto& p)
{
return p._name.empty();
});
std::count
, std::count_if
Reference: https://en.cppreference.com/w/cpp/algorithm/count
- Example:
1
2
3
4struct player_status
{
int _level;
};1
2
3
4
5
6
7const auto high_level_players = std::count_if(
std::begin(players),
std::end(players),
[](const auto& p)
{
return p._level > 70;
});
std::partition
Reference: https://en.cppreference.com/w/cpp/algorithm/partition
- Example:
1
2
3
4
5
6
7
8std::vector<std::string> ids("U.1001", "U.1002", "G.AAAA", "G.AAAB", "U.1003");
const auto it = std::partition(
std::begin(ids), std::end(ids),
[](const auto& x)
{
return x.size() > 2 && x[0] == 'U' && x[1] == '.';
});1
2
3
4
5
6
7
8
9std::for_each(std::begin(ids), it, [](const auto& x)
{
std::cout << "user: " << x << '\n';
});
std::for_each(it, std::end(ids), [](const auto& x)
{
std::cout << "group: " << x << '\n';
});
std::max_element
Reference: https://en.cppreference.com/w/cpp/algorithm/max_element
- Example:
1
2
3
4
5
6
7
8std::vector<music_track> songs{ /* ... */ };
const auto& longest_song = std::max_element(
std::begin(songs), std::end(songs),
[](const auto& a, const auto& b)
{
return a._duration < b._duration;
});
Summary
- Any algorithm that takes a
FunctionObject
orCallable
as a parameter can benefit from lambda expressions - Your code becomes more expressive and shorter
- Since C++17, algorithms can be automatically parallelized thanks to execution policies
- https://en.cppreference.com/w/cpp/algorithm
- https://en.cppreference.com/w/cpp/numeric
Lambdas and Multithreading
std::thread
std::thread
allows the creation of a thread that executes a givenCallable
object1
2
3
4
5
6
7
8std::thread t{[]
{
std::this_thread::sleep_for(100ms);
std::cout << "world\n";
}};
std::cout << "hello ";
t.join();Lambdas make it easy to invoke member functions asynchronously
1
2
3
4
5
6
7
8
9foo my_foo;
std::thread t0{[&]{ my_foo.f0(); }};
std::thread t1{[&]{ my_foo.f1(); }};
// ...
t0.join();
t1.join();
std::future
std::future<T>
allows to asynchronously compute aT
and get it later in the futureInstances of
std::future
running in separate threads can be spawned usingstd::async( std::launch async, /* */ )
Examples:
1
2
3
4
5
6
7
8
9auto f = std::async(std::launch::async, []
{
std::this_thread::sleep_for(1000ms);
return 42;
});
something();
assert(f.get() == 42);
boost::future
boost::future
allows asynchronous computations to be chained. This is proposed forstd::future
as well1
2
3
4
5
6
7
8auto result = boost::async(boost::launch::async, []
{
return html_get_request("someurl.com");
})
.then([](auto& f)
{
return process(f.get());
});
boost::when_all
1 | auto result = boost::when_all( |
Lambdas and Tuples
std::tuple<Ts ...>
is a fixed-size collection of heterogeneous values1
std::tuple<int, float, char> t{42, 1234.f, 'a'};
Lambdas can help us:
- Invoke an existing
FunctionObject
with the elements of atuple
- Iterate over a
tuple
‘s elements
- Invoke an existing
std::apply
Reference: https://en.cppreference.com/w/cpp/utility/applyExample:
1
2
3
4
5
6
7
8std::tuple<int, int> t0{1, 3};
const auto sum = std::apply(
[](int a, int b){ return a + b; },
t0
);
assert(sum == 4);Lambdas make it easy to extract the
sum
functionality and apply it to multiple tuples1
2
3
4
5
6
7const auto sum = [](const auto& t)
{
return std::apply([](int a, int b){ return a + b; }, t);
};
assert(sum(std::make_tuple(1, 2)) == 3);
assert(sum(std::make_tuple(50, 50)) == 100);By combining lambdas and C++17 fold expressions, we can easily iterate over a tuple’s elements
1
2
3
4
5
6std::tuple<int, char> t{42, 'X'};
std::apply([](const auto& ... xs)
{
(std::cout << ... <<< xs);
}, t);42X
- Generalized by lambdas
1
2
3
4
5
6
7const auto for_tuple = [](auto&& f, auto&& t)
{
std::apply([](auto&& ... xs)
{
f(std::forward<decltype(xs)>(xs), ...);
}, std::forward<decltype(t)>(t));
};1
2
3
4
5std::tuple<int, char> t{42, 'X'};
for_tuple([](const auto& x)
{
std::cout << x << ' ';
}, t);
- Generalized by lambdas
Summary
- Learned that most algorithms in the Standard Library greatly benefit from lambda expressions
- Learned that spawning threads and futures with lambdas is an intuitive way of creating asynchronous computations
- Understood that combining lambdas and
std::apply
allows us to easily manipulatestd::tuple
‘s elements
Guidelines
- Unless you have a very commonly used or long predicate, prefer lambdas instead of struct/non-member functions for algorithms
- For simple asynchronous computations, prefer in-place lambdas
- More complicated pipelines will benefit from named function objects