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

  • Here's a One-Stop Shop for .NET 5 Improvements

    Culled from reams of Microsoft documentation, here's a high-level summary of what's new for performance, networking, diagnostics and more, along with links to the nitty-gritty details for those wanting to dig in more.

  • Azure SQL Database Ranked Among Top 3 Databases of 2020

    Microsoft touted the inclusion of Azure SQL Database among the top three databases of 2020 in a popularity ranking by DB-Engines, which collects and manages information about database management systems, updating its lists monthly.

  • Time Tracker Says VS Code Is No. 1 Editor for Devs, Some Working 15+ Hours Per Day

    WakaTime, which does time tracking for programmers, released data for 2020 showing that Visual Studio Code is by far the top editor/IDE used by its coders, some of whom are hacking away for more than 15 hours per day.

  • VS Code Java Project Explorer Gets Better

    The recently introduced project view for managing Java projects in Visual Studio Code received several enhancements in the latest update to Java functionality provided in Microsoft's popular open source, cross-platform code editor.

Upcoming Events