DEV Community

Cover image for Lambda Functions in C++
Pratik Parvati
Pratik Parvati

Posted on • Updated on

Lambda Functions in C++

Lambda Functions in C++

C++ Lambdas are an important feature in modern C++, Lambdas are fancy name for anonymous function object which are used widely for writing in-place functions (i.e, right at the location where it is invoked or passed as an argument to a function) and generally enhances the functionality of C++.

A lambda consists typically of three parts: a capture list [], an optional parameter list () and a body {}; the expression [](){}(); declares an empty lambda and immediately executes it, of course this is completely useless expression. The basic prototype of lambda expressions is the following.

[capture clause] (parameters) -> return-type
{       
    // body
}
Enter fullscreen mode Exit fullscreen mode
  • capture clause - these are variables that are copied inside the lambda to be used in the code.
  • parameters - these are the arguments that are passed to the lambda at execution time.
  • body - the body of the function, which is the same as in regular functions.
  • return-type - return type of the function.

Here’s a simple example:

auto addition = [](int x, int y) { return x + y; };
Enter fullscreen mode Exit fullscreen mode

where, [](int x, int y) { return x + y; } is the lambda expression.

Motivation behind lambda functions

The primary motivation for the lambda functions is to provide flexible and convenient means to define unnamed function objects for STL algorithms. Typically STL algorithms operate on container elements via function objects, these function objects are passed as arguments to the algorithms.

NOTE: If you are not aware of function objects in C++, I highly recommend reading this article before going ahead.

Consider the following code as an example

class addVecFunctor
{
private:
    int m_x;

public:
    addVecFunctor(int x) : m_x(x) {}

    void operator()(int x)
    {
        std::cout << x + m_x << ' ';
    }
};

int main()
{
    std::vector<int> vec{10, 20, 30, 40, 50};
    std::for_each(vec.begin(), vec.end(), addVecFunctor(100));
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

If you only use addVecFunctor() once and in that specific place, it doesn't make sense to write an entire class just to do something trivial and one-time. C++11 introduces lambdas allow you to write an inline, anonymous functor to replace the class addVecFunctor.

The above code can be replaced using lambda function as

int main()
{
    std::vector<int> vec{10, 20, 30, 40, 50};
    int addNum = 100;
    std::for_each(vec.begin(), vec.end(), [&addNum](int x)
                  { std::cout << addNum + x << ' '; });
    return 0;
}
Enter fullscreen mode Exit fullscreen mode
Lambda under the hood

The compiler replaces the line std::for_each(vec.begin(), vec.end(), [](int x) { std::cout << x << ' ';}); for the above code as:

class anonymous
{
private:
    int &addNum;

public:
    anonymous(int &_addNum) : addNum(_addNum) {}
    inline auto operator()(int x) const
    {
        std::cout << addNum + x << ' ';
    }
};

int addNum = 100;
std::for_each(vec.begin(), vec.end(), anonymous(addNum));
Enter fullscreen mode Exit fullscreen mode

The same is observed in cppinsight(displays compiler view of the code) here. The compiler generates an unique closure object (or function object) to be replaced as an argument inside for_each function. Capture list will become a constructor argument inside the closure class and the lambda parameters(as received from the vector iterator) will become an argument to operator().

Lambda capture list

These are some examples of typical capture lists:

Capture lists Description
[] Empty capture list, nothing will be captured.
[foo] Capture foo by copy.
[&bar] Capture bar by reference.
[foo, &bar] Capture foo by-copy and bar by-reference.
[=] Capture anything named from the enclosing scope by-copy.
[&] Capture anything named from the enclosing scope by-reference.
[&, foo, bar] Capture anything named from the enclosing scope by reference, except foo and bar which must be captured by-copy.
[=, &foo] Capture anything named from the enclosing scope by copy, except foo which must be captured by-reference

Capturing member variables inside lambda function

If you try to capture member variable directly by value or reference, then it will not work

class addVector
{
    // vector sum
    int sum = 0;
public:
    int getSum()
    {
        return sum;
    }
    void update(std::vector<int>& vec)
    {
        // Capturing member variable by value will not work
        // Will result in Compile Error
        std::for_each(vec.begin(), vec.end(), [sum](int element){
                sum += element; // Accessing member variable from outer scope
        });
    }
};
Enter fullscreen mode Exit fullscreen mode

since sum is a non-static member variable, It will result in compile error. Hence, to capture the member variables inside lambda function, capture the this pointer by value.

std::for_each(vec.begin(), vec.end(), [this](int element){
    sum += element;
}
Enter fullscreen mode Exit fullscreen mode

Capturing this pointer inside lambda function will automatically capture all the member variables for this object inside lambda.

Lambda return type

Often, the return type does not need to be specified because the compiler can deduce it. The compiler can easily deduce the return type if you only have one return statement. If a lambda body does not have a return statement, the lambda’s return type is void. If a lambda body consists of just a single return-statement, the lambda’s return type is the type of the return’s expression. It may be necessary or preferable to specify it explicitly for more complex lambdas.

auto func = [](int i){ return i; }; // return type is int
Enter fullscreen mode Exit fullscreen mode

mutable keyword for lambda

The lambda function call operator is const expression; which means lambda requires mutable keyword to modify any capture by value variable inside a capture list.

For instance, the below code throws compiler error

int val;
auto bad_lambda = [val]() { val += 100; };
Enter fullscreen mode Exit fullscreen mode

The copy captures can be made writable if the lambda is declared as mutable; any change you do to the copy capture will be carried over to the next execution of the same lambda.

int val{100};

auto good_lambda = [val]() mutable {
    std::cout << val++ << std::endl;
};

// the lambda remembers the state of the closure object
good_lambda(); // prints 100
good_lambda(); // prints 101
good_lambda(); // prints 102

// the original variable outside of the lambda is unchanged
std::cout << val << std::endl; // prints 100
Enter fullscreen mode Exit fullscreen mode

Naming a Lambda Function

There are three possible ways to give lambda a name.

1. Using std::function<>

we can use std::function<> to hold the function pointer. The below code snippet uses std::function<> which takes integer as function argument.

std::function<void(int)> print = [](int val)
{
    std::cout << val << std::endl;
};
// calling lambda function with explicit std::function handler
print(100);
Enter fullscreen mode Exit fullscreen mode
2. Using auto keyword

The lambda can be simply assigned a name by using C++ auto keyword; this is the simplest way of assigning a name to a lambda.

auto print = [](int val)
{
    std::cout << val << std::endl;
};
// calling lambda function 
print(100);
Enter fullscreen mode Exit fullscreen mode
3. Using C function pointers

Lambdas are just functions and can be assigned to a 'C' style function pointers; this is seriously discouraged.

void(*print)(int) = [](int val)
{
    std::cout << val << std::endl;
};
// calling function pointer
print(100);
Enter fullscreen mode Exit fullscreen mode

Immediately invoke a C++ lambda (IIFE)

An immediately invoked function expression (IIFE) is a function, which runs as soon as it is created. Since lambdas are kept anonymous, they are used and forgotten immediately. It is good way of declaring variables and executing code without polluting the global namespace. The parenthesis immediately following the closing braces invokes the function immediately and hence the name immediately invoking function expression.

#include <iostream>

auto main() -> int {

  [](){
    std::cout <<"Hello IIFE" <<"\n";
  }(); // Invoking the lambda immediately, so it'll print "Hello IIFE"

  return 1;
}
Enter fullscreen mode Exit fullscreen mode

The above code works perfectly fine and it is evident that C++ IIFE useful for converting a series of statements into an expression. This technique uses an anonymous function inside parentheses to convert it from a declaration to an expression, which is executed immediately. Also note that, everything that you define within the IIFE can only be accessed within the IIFE.

Assembly code generated by the lambda

Whether or not you capture variables, the assembler code is the same as for a regular class. The only exception is that when capturing variables, the function Object() is inlined because it will not be used again.

Please read this article for deeper dive.

Finally, Lambda functions are very useful, even necessary, for effective use of modern C++ libraries and offer a clean and concise alternative to writing functions. They can be declared in various ways based on the needs of the users by utilizing capture clauses, parameters and return type(which are optional).

Discussion (1)

Collapse
ats1999 profile image
Rahul kumar

Very interesting article.