New Age C++

Using Lambda Expressions for Shorter, More Readable C++ Code

Even for C++'s ancestor, C, one of its most valued features was the ability to declare functions as parameters for other functions or procedures.

This month, I'll focus on lambda expressions in C++. Lambdas are lightweight anonymous functions usually defined where they're used.

Why C++ Needs Lambda Expressions
Lambdas were a natural evolution. Even for C++'s ancestor, C, one of its most valued features was the ability to declare functions as parameters for other functions or procedures (similar to C# delegates). An example can be seen in Listing 1.

C++ introduced function objects, or functors. Functors are classes that overload the operator(). Since they're classes, functors can leverage object-oriented features like inheritance, polymorphism and so on. Listing 2 shows a typical functor.

Unlike C pointers to functions, functors can keep state (i.e., in private member variables, as you'll see in Listing 3). The downside compared with C comes from all those extra lines beyond the operator() definition, necessary to declare the function as a class. Both share the same eventual drawback: neither allows you to define the function just where you need to use it.

Other programming languages like Haskell, C#, Erlang or F# enable function definitions right where they're used. These are known as lambda expressions because its syntax is inspired in lambda calculus. Lambda expressions came to C++ in its ISO C++11 standard specification.

Anatomy of a C++ Lambda Expression
Listing 4 shows the lambda-based version of Listing 3.

A lambda definition starts with context-variable capture between brackets. In the two lambdas in Listing 4 the brackets are empty, so I'm not capturing anything. But I could have defined the second lambda in this way:

// even_count is initialized in 0.
unsigned even_counter(0);


// even_counter is captured by reference in the lambda.
for_each(begin(l), end(l), [&even_counter] (int n) {
           if (n%2)
             cout << n << " is odd." << endl;
           else
             ++even_counter;
         });
         

cout << even_counter << " even elements skipped." << endl;

Thus, the lambda can access a variable from the context in which it's defined and even modify it. Capture by reference is how it's done in C#, but it's not the only way to do it in C++; if the last example had been [even_counter] (that is to say, without the ampersand), even_counter would have been captured by value.

We can capture more than one variable, separating them with commas:

// this lambda captures i by value and j and k by reference.
[i, &j, &k] (...) { ... };

Context variables captured by value can't be modified in the body of the lambda unless we add the "mutable" modifier to the lambda definition:

unsigned p(1);


cout << "p in lambda ends being " << [p] (int i) mutable { return p+= i; }(5) << ", but p outside remains " << p;
// Output: "p in lambda ends being 6, but p outside remains 1"

Capturing by value happens at lambda definition. If I modified p in the above example after the lambda is defined but before it's executed, the inner p would end with the same value (6, in the example).

Lambdas are similar to functors: the variables captured by reference are like private pointers, and the ones captured by value are like private variables.

There are some shortcuts: "[&]" means "capture all variables accessible in scope by reference". "[=]" does the same, but by value. If the lambda is defined inside a class, by capturing "[this]" we get access to any class member, including private ones.

Is it acceptable to break class encapsulation from inside a lambda expression? Yes, as long as it makes more sense to define the function within the context of a class function, rather than on its own.

The returned type of a lambda is inferred when the body consists only of a single return statement (similar to the repurposed auto declarator in C++11). It's assumed to be void otherwise, so the body can't contain any return statement. If that's not the case, it must be specified. For example:

// myLambda receives an integer and returns bool.
auto myLambda = [](int i) -> bool { if (i%2) return true; else return false; };

Lambdas can declare exceptions to be thrown:

// This lambda potentially throws runtime_error.
auto myLambda = []() throws(runtime_error) -> bool { ... };

// This lambda doesn't throw exceptions.
auto myLambda = []() throws() -> bool { ... };

// This lambda could throw any exception.
auto myLambda = []() -> bool { ... };

Lambda Benefits
Lambdas are lightweight, nameless functions that can be defined just-in-place where they are used. Lambdas didn't come to C++ as a specific-purpose feature (i.e., functional programming): the existing STL components, like the for_each algorithm, and its new ones like <thread> or <future>, allow you to specify function arguments as lambdas beside C-like functions and functors. Unless you need a function available in more than one place, you'll feel more comfortable using lambdas as arguments of STL components, similar to the simplification gained in Listing 4 compared with Listing 3.

In a future article, I'll show how lambdas can be easily used to implement high-order functions. These are functions that either receive or return functions, and eventually both. Stay tuned!

About the Author

Diego Dagum is a software architect and developer with more than 20 years of experience. He can be reached at [email protected].

comments powered by Disqus

Featured

  • Full Stack Hands-On Development with .NET

    In the fast-paced realm of modern software development, proficiency across a full stack of technologies is not just beneficial, it's essential. Microsoft has an entire stack of open source development components in its .NET platform (formerly known as .NET Core) that can be used to build an end-to-end set of applications.

  • .NET-Centric Uno Platform Debuts 'Single Project' for 9 Targets

    "We've reduced the complexity of project files and eliminated the need for explicit NuGet package references, separate project libraries, or 'shared' projects."

  • Creating Reactive Applications in .NET

    In modern applications, data is being retrieved in asynchronous, real-time streams, as traditional pull requests where the clients asks for data from the server are becoming a thing of the past.

  • AI for GitHub Collaboration? Maybe Not So Much

    No doubt GitHub Copilot has been a boon for developers, but AI might not be the best tool for collaboration, according to developers weighing in on a recent social media post from the GitHub team.

  • Visual Studio 2022 Getting VS Code 'Command Palette' Equivalent

    As any Visual Studio Code user knows, the editor's command palette is a powerful tool for getting things done quickly, without having to navigate through menus and dialogs. Now, we learn how an equivalent is coming for Microsoft's flagship Visual Studio IDE, invoked by the same familiar Ctrl+Shift+P keyboard shortcut.

Subscribe on YouTube