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

Subscribe on YouTube