Michael Rizkalla
by Michael Rizkalla

Since C++11 standard, we were allowed to instantiate and use what is called a lambda expression. Lambda expressions formally are defined as Closure type which is new to C++. Function objects (Callable objects) have been existing for a long time and can be passed around as function arguments and called later. Callable objects are usually implemented by overloading operator() of a class or struct to act like a function. See the following example for a demonstration

struct Greeter {
    // Greeter can act like a function to greet a name
    void operator()(const std::string& name) {
        std::cout << "Hello " << name << '\n';
    }
};

// Greeter greeter;
// greeter("YourName");

But if function/callable objects existed already, what do lambda expressions offer to us? TL;DR: A lot of reduced boilerplate that would have been implemented to account for different use cases.

Lambda expression definition

Lambda expression implements closures. Closures allows the existence of a first-class functions, which is treating functions as first class citizens, allowing to pass functions as arguments. Closures also map environment variables as required by either value or reference.

Declaring a lambda expression as of C++20 standard

According to the C++ standard, lambda expressions can be declared as follows:

auto my_lambda = [/*captured variables*/](/*parameters*/){ /*function body*/ };
auto my_template_lambda = [/*captured variables*/] </*templates*/> (/*parameters*/){ /*function body*/ };
auto my_ret_lambda = [/*captured variables*/] (/*parameters*/) -> ret /* return type */ { /*function body*/ };
auto my_concept_lambda = [/*captured variables*/] (/*parameters*/) -> ret /* return type */ requires /* concept */ { /*function body*/ };

Please note that parameters can be declared with keyword auto, which makes the lambda object generic because /auto/ works similar to a template parameter. Also, note that the captured by reference variable, if went out of scope, will lead to undefined behaviour if used by the lambda expression.

Lambda expressions breakdown

The remaining of this article will use C++ Insights to demonstrate how the compiler views our lambda expressions, and we will demonstrate how to write the required task before lambdas and after lambdas so we can grasp the difference :).

Lets assume we want to write a callable object that sum two integer variables and keeps track of how many times it was called in a global variable.

std::size_t tracker = 0;

struct Sum {
    inline constexpr int operator()(int&& first, int&& second) const {
        tracker++;
        return first + second;
    }
}
Sum sum{};

This can be wirtten as a lambda expressions as follows:

std::size_t tracker = 0;
auto my_first_lambda = [&tracker](int&& first, int&& second) {tracker++; return first + second; };

It’s really obvious how a lot of boiler-plate was removed. Now, it’s time to breakdown the lambda function using C++ Insights

// Copied directly from C++ Insights
std::size_t tracker = static_cast<unsigned long>(0); 
class __lambda_7_25
{
    public: 
    inline /*constexpr */ int operator()(int && first, int && second) const
    {
        tracker++;
        return first + second;
    }

    private: 
    std::size_t & tracker;

    public:
    __lambda_7_25(std::size_t & _tracker)
    : tracker{_tracker}
    {}

};

__lambda_7_25 my_first_lambda = __lambda_7_25{tracker};

We can observe that the compiler has generated all boilerplate code for us. The lambda class captured the variable by reference as instructred, so it can be passed around to other function and be operated on. This is a big advantage as the captured variable can be in any scope when captured and we do not need a global one.

Extend our struct and lambda to accept generic types

struct Sum {
    template< typename _Ty > 
    // Can use require to ensure type is integral but no in the scope of this article
    inline constexpr auto operator()(_Ty first, _Ty second) const {
        tracker++;
        return first + second;
    }
}
Sum sum{};

// A Lambda expressions equivalent
auto my_lambda = [&tracker]<typename _Ty>(_Ty first, _Ty second) {tracker++; return first + second; };
// Or (with explicit return type)
auto my_lambda = [&tracker]<typename _Ty>(_Ty first, _Ty second) -> _Ty {tracker++; return first + second; };

It seems simple enought, right :D? But, cannot we use auto to make our lambda generic instead of explicit template. Yes we can, but this will associate each parameter with its own deduced type so we will be able to pass an int and a float, which is not the purpose of our task. However, this is a good example to show the concepts usage in a lambda concept.

auto my_lambda = [&tracker](auto first, auto second) requires std::same_as<decltype(first), decltype(second)> {tracker++; return first + second; };

Conclusion

As we have seen in this short article: Lambda functions provide an alternative implementation to function objects with less boilerplate. Lambda functions provide the ability to capture variables by either reference or value. Lambda functions can be passed around as function arguments.