C++17 In Detail

25 May 2020

5 Advantages of C++ Lambda Expressions and How They Make Your Code Better

Lambda Expressions, C++, Advantages

[](){}

The mixture of brackets in the preceding line become one of the most noticeable indications of Modern C++.
Yep.
Lambda Expressions!
It might sound like I’m trying to create a new blog post about something that everyone knows. Is that true? Do you know all the details of this modern C++ technique?

In this article, you’ll learn five advantages of Lambdas. Let’s start.

1. Lambdas Make Code More Readable

The first point might sound quite obvious, but it’s always good to appreciate the fact that since C++11, we can write more compact code.

For example, recently, I stumbled upon some cases of C++03/C++0x with bind expressions and predefined helper functors from the Standard Library.

Have a look at the code:

#include <algorithm>
#include <functional>
#include <vector>

int main() {
    using std::placeholders::_1;

    const std::vector<int> v { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    const auto val = std::count_if(v.begin(), v.end(),
                               std::bind(std::logical_and<bool>(),
                               std::bind(std::greater<int>(),_1, 2),
                               std::bind(std::less_equal<int>(),_1,6)));

    return val;                                        
}

Play with the code @Compiler Explorer

Can you immediately tell what the final value of val is?

Let’s now rewrite this into lambda expression:

#include <algorithm>
#include <vector>

int main() {
    std::vector<int> v { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    const auto val = std::count_if(v.begin(), v.end(), 
                        [](int v) { return v > 2 && v <= 6;});

    return val;                                        
}

Isn’t that better?

Play with the code @Compiler Explorer

Not only we have shorter syntax for the anonymous function object, but we could even reduce one include statement (as there’s no need for <functional>any more).

In C++03, it was convenient to use predefined helpers to build those callable objects on the fly. They were handy and allowed you even to compose functionalities to get some complex conditions or operations. However, the main issue is the hard-to-learn syntax. You can of course still use them, even with C++17 or C++20 code (and for places where the use of lambdas is not possible), but I guess that their application for complex scenarios is a bit limited now. In most cases, it’s far easier to use lambdas.

I bet you can list a lot of examples from your projects where applying lambda expressions made code much cleaner and easier to read.

Regarding the readability, we also have another part: locality.

2. Lambdas Improve Locality of the Code

In C++03, you had to create functions or functors that could be far away from the place where you passed them as callable objects.

This is hard to show on simple artificial examples, but you can imagine a large source file, with more than a thousand lines of code. The code organisation might cause that functors could be located in one place of a file (for example on top). Then the use of a functor could be hundreds of lines further or earlier in the code if you wanted to see the definition of a functor you had to navigate to a completely different place in the file. Such jumping might slow your productivity.

Jumping around a source file

We should also add one more topic to the first and the second point. Lambdas improve locality, readability, but there’s also the naming part. Since lambdas are anonymous, there’s no need for you to select the meaningful name for all of your small functions or functors.

3. Lambdas Allow to Store State Easily

Let’s have a look at a case where you’d like to modify a default comparison operation for std::sort with an invocation counter.

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec { 0, 5, 2, 9, 7, 6, 1, 3, 4, 8 };

    size_t compCounter = 0;
    std::sort(vec.begin(), vec.end(), [&compCounter](int a, int b) {
        ++compCounter;
        return a < b;
    });

    std::cout << "number of comparisons: " << compCounter << '\n';

    for (auto& v : vec)
        std::cout << v << ", ";
}

Play with the code @Compiler Explorer

As you can see, we can capture a local variable and then use it across all invocations of the binary comparator. Such behaviour is not possible with regular functions (unless you use globals of course), but it’s also not straightforward with custom functors types. Lambdas make it very natural and also very convenient to use.

In the example I captured compCounter by reference. This approach works, but if your lambda runs asynchronously or on different threads then you need to pay attention for dangling and synchronisation issues.

4. Lambdas Allow Several Overloads in the Same Place

This is one of the coolest examples not just related to lambdas, but also to several major Modern C++ features (primarily available in C++17):

Have a look:

#include <iostream>
#include <string>
#include <variant>

template<class... Ts> struct overload : Ts... { using Ts::operator()...; };
template<class... Ts> overload(Ts...) -> overload<Ts...>;

int main() {
    std::variant<int, float, std::string> intFloatString { "Hello" };
    std::visit(overload  {
        [](const int& i) { std::cout << "int: " << i; },
        [](const float& f) { std::cout << "float: " << f; },
        [](const std::string& s) { std::cout << "string: " << s; }
      },
      intFloatString
    );        
}

Play with the code @Compiler Explorer

The above example is a handy approach to build a callable object with all possible overloads for variant types on the fly. The overloaded pattern is conceptually equivalent to the following structure:

struct PrintVisitor
{
    void operator()(int& i) const {
        std::cout << "int: " << i; }

    void operator()(float& f) const {
        std::cout << "float: " << f;
    }

    void operator()(const std::string& s) const {
        std::cout << "string: " << s;
    }
};

You can learn more about this pattern in my separate article, see the reference section.

Additionally, it’s also possible to write a compact generic lambda that works for all types held in the variant. This can support runtime polymorphism based on std::variant/std::visit approach.

#include <variant>

struct Circle { void Draw() const { } };
struct Square { void Draw() const { } };
struct Triangle { void Draw() const { } };

int main() {
    std::variant<Circle, Square, Triangle> shape;
    shape = Triangle{};
    auto callDraw = [](auto& sh) { sh.Draw(); };
    std::visit(callDraw, shape);
}

Play with the code @Compiler Explorer

This technique is an alternative to runtime polymorphism based on virtual functions. Here we can work with unrelated types. There’s no need for a common base class. See the Reference section for more links about this pattern.

5. Lambdas Get Better with Each Revision of C++!

You might think that lambdas were introduced in C++11 and that’s all, nothing changed. But it’s not true.

Here’s the list of major features related to lambdas that we got with recent C++ Standards:

  • C++14
    • Generic lambdas - you can pass auto argument, and then the compiler expands this code into a function template.
    • Capture with initialiser - with this feature you can capture not only existing variables from the outer scope, but also create new state variables for lambdas. This also allowed capturing moveable only types.
  • C++17
    • constexpr lambdas - in C++17 your lambdas can work in a constexpr context!
    • Capturing this improvements - With C++17 you can capture *this OBJECT by copy,  avoiding dangling when returning the lambda from a member function or store it. (Thanks to Peter Sommerlad for improved wording and checking)
  • C++20
    • Template lambdas - improvements to generic lambdas which offers more control over the input template argument.
    • Lambdas and concepts - Lambdas can also work with constrained auto and Concepts, so they are as flexible as functors as template functions
    • Lambdas in unevaluated contexts - you can now create a map or a set and use a lambda as a predicate.

Plus some smaller things and fixes.

Summary

With this article, we refreshed some basic ideas and advantages of lambda expressions. We reviewed improved readability, locality, ability to hold state throughout all invocations. We event went a bit further and examined the overloaded pattern and list all the features from recent C++ Standards. I guess we can summarise all points into the single statement:

C++ Lambda Expressions make your code more readable and simple

  • Do you have examples where lambda expression “shines”?
  • Or maybe you still prefer predefined functors and helpers from the Standard Library?
  • Do you see other benefits of Lambdas?

Let us know your opinions in comments.

If You Want to Know More

Last year, in 2019, I published two extensive articles about lambda expression. They were based on a presentation on our local Cracow C++ User Group:

Together, those articles become one of the most popular content, and so far, they generated over 86 thousand views!

Later, I took the content from those articles and created an ebook that you can get on Leanpub.
But it’s just part of the story. After the launch, I managed to provide several significant updates, new sections, more examples and better descriptions.
Right now, the book is massively improved and packed with more than 2X of the original content.

Lambda Story

You can get it here:
Get C++ Lambda Story @Leanpub

The book is also available in a package with my C++17 book:
C++17 in Detail and Lambda Story Bundle

And also you get get it for free if you join my Patreon page:
See Extra Benefits for Patrons and Get Lambda Story for Free

C++17 In Detail
© 2017, Bartlomiej Filipek, Blogger platform
Disclaimer: Any opinions expressed herein are in no way representative of those of my employers. All data and information provided on this site is for informational purposes only. I try to write complete and accurate articles, but the web-site will not be liable for any errors, omissions, or delays in this information or any losses, injuries, or damages arising from its display or use.
This site contains ads or referral links, which provide me with a commission. Thank you for your understanding.