0%

Lambdas and the Standard Library in C++

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 parameters

  • Most of them reside in the <algorithm> header

  • Others, like std::accumulate, reside in <numeric>

  • std::for_each
    Reference: https://en.cppreference.com/w/cpp/algorithm/for_each

    1
    2
    3
    4
    5
    std::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
      3
      std::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
    7
    struct person
    {
    std::string _name;
    int _age;
    };

    std::vector<person> ps{ /* ... */ };
    1
    2
    3
    4
    5
    const bool can_drink = std::all_of(
    std::begin(ps), std::end(ps), [](const auto& p)
    {
    return p._age >= 21;
    });
    1
    2
    3
    4
    5
    6
    const 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
    4
    struct player_status
    {
    int _level;
    };
    1
    2
    3
    4
    5
    6
    7
    const 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
    8
    std::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
    9
    std::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
    8
    std::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

Lambdas and Multithreading

std::thread

  • std::thread allows the creation of a thread that executes a given Callable object

    1
    2
    3
    4
    5
    6
    7
    8
    std::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
    9
    foo 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 a T and get it later in the future

  • Instances of std::future running in separate threads can be spawned using std::async( std::launch async, /* */ )

  • Examples:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    auto 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 for std::future as well
    1
    2
    3
    4
    5
    6
    7
    8
    auto result = boost::async(boost::launch::async, []
    {
    return html_get_request("someurl.com");
    })
    .then([](auto& f)
    {
    return process(f.get());
    });

boost::when_all

1
2
3
4
5
6
7
8
9
10
11
12
auto result = boost::when_all(
boost::async(boost::launch::async, []
{
return html_get_request("url0.com");
}),
boost::async(boost::launch::async, []
{
return html_get_request("url1.com");
}))
.then([](auto& f){
return process(f.get());
});

Lambdas and Tuples

  • std::tuple<Ts ...> is a fixed-size collection of heterogeneous values

    1
    std::tuple<int, float, char> t{42, 1234.f, 'a'};
  • Lambdas can help us:

    • Invoke an existing FunctionObject with the elements of a tuple
    • Iterate over a tuple‘s elements
  • std::apply
    Reference: https://en.cppreference.com/w/cpp/utility/apply

  • Example:

    1
    2
    3
    4
    5
    6
    7
    8
    std::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 tuples

    1
    2
    3
    4
    5
    6
    7
    const 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
    6
    std::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
      7
      const 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
      5
      std::tuple<int, char> t{42, 'X'};
      for_tuple([](const auto& x)
      {
      std::cout << x << ' ';
      }, t);

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 manipulate std::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