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

  • IDE Irony: Coding Errors Cause 'Critical' Vulnerability in Visual Studio

    In a larger-than-normal Patch Tuesday, Microsoft warned of a "critical" vulnerability in Visual Studio that should be fixed immediately if automatic patching isn't enabled, ironically caused by coding errors.

  • Building Blazor Applications

    A trio of Blazor experts will conduct a full-day workshop for devs to learn everything about the tech a a March developer conference in Las Vegas keynoted by Microsoft execs and featuring many Microsoft devs.

  • Gradient Boosting Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the gradient boosting regression technique, where the goal is to predict a single numeric value. Compared to existing library implementations of gradient boosting regression, a from-scratch implementation allows much easier customization and integration with other .NET systems.

  • Microsoft Execs to Tackle AI and Cloud in Dev Conference Keynotes

    AI unsurprisingly is all over keynotes that Microsoft execs will helm to kick off the Visual Studio Live! developer conference in Las Vegas, March 10-14, which the company described as "a must-attend event."

  • Copilot Agentic AI Dev Environment Opens Up to All

    Microsoft removed waitlist restrictions for some of its most advanced GenAI tech, Copilot Workspace, recently made available as a technical preview.

Subscribe on YouTube