Lamda function in C++

No alt text provided for this image


Lambda functions or lambda expressions are a feature that was introduced in C++11. It is a concise way to define small and anonymous functions that can be used anywhere in the program. The lambda functions are also known as closures or anonymous functions. The name lambda comes from the lambda calculus, which is a formal system for expressing computation based on function abstraction and application. The lambda function has been a popular feature in functional programming languages such as Java, Kotlin, Lisp, Scheme, and Python. In this article, we will discuss the motivation behind having lambda functions, their history, general format, advantages, limitations, suitable scenarios, and examples.

Motivation:

Before C++11, to define a function in C++, we need to write a complete function definition with the function name, return type, and parameter list. This can be cumbersome when we need to define a small function, especially when the function is needed only once or a few times in the code. In such cases, a lambda function provides a more concise way of defining functions.

History:

The concept of lambda functions can be traced back to the lambda calculus, a mathematical notation for expressing computation based on function abstraction and application. The first programming language to incorporate lambda functions was Lisp, which was invented in the late 1950s. Since then, lambda functions have been a popular feature in functional programming languages such as Java, Kotlin, Scheme, Python, and Haskell. The C++ language did not have lambda functions until the C++11 standard.

General format:

The general format of a lambda function is as follows:

[capture clause](parameter list) -> return type { function body  }        

Capture Clause:

The capture clause allows you to capture variables from the enclosing scope. It can be either explicit or implicit. In the explicit capture, you can specify the variables to be captured by their names or by reference or value. In the implicit capture, the compiler automatically detects the variables used inside the lambda function and captures them by value. The capture clause is optional and can be omitted if the lambda function does not use any variables from the enclosing scope.

Keep the following in mind when you are using this syntax for the [Capture Clause ]:

  1. [] it means that you will not provide anything to lambda.
  2. [&] means you have some references to deal with.
  3. [=] it is used to make the copy.
  4. [this] is used to the enclosing class.

  • If we try to capture member variable directly by value or reference, then it will not work.
  • To capture the member variables inside lambda function, capture the “this” pointer by value i.e. Capturing this pointer inside lambda function will automatically capture all the member variables for this object inside lambda.

Parameter List:

The parameter list contains the parameters that the lambda function takes. It can be empty if the lambda function does not require any parameters.

Return Type:

The return type is optional and can be omitted if the lambda function does not return any value. If the lambda function returns a value, the return type must be specified.

Function Body:

The function body contains the code that the lambda function executes. It can be a single statement or a block of statements.


Compiler magic on lamda functions?

Lambda functions in C++ are treated as special anonymous function objects by the compiler. When you define a lambda function, the compiler generates a unique closure type for that lambda, which encapsulates the lambda's captured variables (if any) and its body.

The generated closure type is a unique, unnamed type that has an operator() function defined. This operator() function represents the body of the lambda function and can be called like a regular function.

The compiler also performs type inference to determine the return type of the lambda function based on its body. If the return type is not explicitly specified, the compiler deduces it from the return statements in the lambda body.

Lambda functions can capture variables from their enclosing scope, either by value or by reference. The captured variables become members of the closure type, and their values are accessible within the lambda body.

When you invoke a lambda function, you can do so by using the lambda object itself, just like calling a regular function. The compiler generates code to execute the body of the lambda function and handles the necessary variable capturing and scoping.

Lambda functions provide a concise and convenient way to define small, inline functions with local state. They are commonly used as callbacks, for algorithm customization, or for writing functional-style code.

Example 1: variable from enclosing env is captured by value

#include<iostream>
int main() {
? ? int multiplier = 2;


? ? // Lambda function
? ? auto multiply = [multiplier](int num) {
? ? ? ? return num * multiplier;
? ? };


? ? // Using the lambda function as a functor
? ? int result = multiply(5);


? ? std::cout << "Result: " << result << std::endl;


? ? return 0;
}        

In the above example, when the compiler encounters the lambda function, it generates a closure type with an operator() function that corresponds to the body of the lambda. The multiplier variable is captured by value, so a member variable is created in the closure type to store its value.

The lambda function multiply is essentially converted into an instance of the closure type. We can think of it as if we defined a separate class with an operator() function and instantiated an object of that class.

So, the lambda function multiply is converted to something like this (simplified representation):

struct ClosureType {
? ? int multiplier;

? ? int operator()(int num) const {
? ? ? ? return num * multiplier;
? ? }
};

int main() {
? ? int multiplier = 2;
? ? ClosureType closure{multiplier};

? ? int result = closure(5);

? ? std::cout << "Result: " << result << std::endl;

? ? return 0;
}        

The lambda function multiply is converted into an object of the closure type ClosureType. The captured multiplier value is stored in the closure object, and the invocation of the lambda function is replaced with a call to the operator() function of the closure object


Example 2: When variables are captured by reference in a lambda function

#include<iostream>
int main() {
? ? int multiplier = 2;
? ? int additionResult = 0;

? ? // Lambda function capturing variables by reference
? ? auto addAndMultiply = [&multiplier,&additionResult ](int num) {
? ? ? ? additionResult = num + multiplier;
? ? ? ? return num * multiplier;
? ? };

? ? int result = addAndMultiply (5);

? ? std::cout << "multiplication Result: " << result << std::endl;
? ? std::cout << "Additon Result: " << additionResult << std::endl;

? ? return 0;
}
/*
multiplication Result: 10
Additon Result: 7
*/        

In this updated example, the lambda function multiply captures the multiplier variable by reference using the & symbol. The rest of the code remains the same.

When the compiler encounters the lambda function, it generates a closure type with a reference member variable for the captured variable. The operator() function of the closure type still represents the body of the lambda function, but now it uses the captured variable through the reference.

The lambda function multiply is converted to something like this (simplified representation):

#include<iostream>
using namespace std;

struct ClosureType {
? ? int& multiplier;
? ? int& additionResult;

? ? int operator()(int num) const {
? ? ? ? additionResult? = num + multiplier;
? ? ? ? return num * multiplier;
? ? }
};

int main() {
? ? int multiplier = 2;
? ? int additionResult = 0;
? ? ClosureType closure{multiplier,additionResult};

? ? int result = closure(5);

? ? std::cout << "multiplication Result: " << result << std::endl;
? ? std::cout << "Additon Result: " << additionResult << std::endl;

? ? return 0;
}
/*
multiplication Result: 10
Additon Result: 7
*/        

The lambda function multiply is converted into an object of the closure type ClosureType. The captured multiplier and additionResult variables is stored as a reference in the closure object, and the operator() function of the closure object uses that reference to access the captured variable.

Note that capturing variables by reference in a lambda function can lead to dangling references if the referenced variables go out of scope before the closure object. Care should be taken to ensure the captured variables remain valid throughout the lifetime of the closure object.


Example 3: Usage of [this] to capture the enclosing class in a lambda function:

#include<iostream>
class MyClass {
public:
? ? MyClass() : value(10) {}

? ? void doSomething() {
? ? ? ? // Lambda function capturing 'this'
? ? ? ? auto printValue = [this]() {
? ? ? ? ? ? std::cout << "Value: " << value << std::endl;
? ? ? ? };

? ? ? ? printValue(); // Invoke the lambda function
? ? }

private:
? ? int value;
};

int main() {
? ? MyClass obj;
? ? obj.doSomething();

? ? return 0;
}        


Example 4: lambda function capturing all variables by value using [=]:

#include<iostream>

void printNumbers(int num1, int num2) {
? ? // Lambda function capturing all variables by value
? ? auto print = [=]() {
? ? ? ? std::cout << "Numbers: " << num1 << ", " << num2 << std::endl;
? ? };

? ? print(); // Invoke the lambda function
}


int main() {
? ? int a = 5, b = 10;


? ? printNumbers(a, b);


? ? return 0;
}

>        


Advantages of Lamda:

  1. Concise: Lambda functions provide a more concise way of defining small and anonymous functions, thus reducing code verbosity.
  2. Flexibility: Lambda functions can be used anywhere in the program, such as function parameters, return values, and even inside other functions.
  3. Improved Readability: Lambda functions can improve the readability of the code, especially when used in conjunction with the standard library algorithms such as for_each, transform, and accumulate.
  4. Performance: Lambda functions can improve performance by reducing the need for intermediate variables and function calls.

Limitations of Lamda:

  1. Complex Syntax: The syntax of lambda functions can be complex, especially when using capture clauses and parameter lists.
  2. Compiler Support: Some older compilers may not support lambda functions, so care must be taken to ensure that the code is portable across different compilers.
  3. When the function pointer is required: Lambda functions cannot be converted to function pointers. Therefore, they cannot be used in places where a function pointer is required, such as in callbacks.
  4. When the function has a variable number of arguments: Lambda functions must have a fixed number of arguments. Therefore, they cannot be used for functions with a variable number of arguments, such as the printf() function.
  5. When the function requires a specific calling convention: Lambda functions cannot be used for functions that require a specific calling convention, such as __stdcall or __fastcall.

__stdcall and __fastcall are calling conventions that define how function calls are made and how function parameters are passed between the caller and the callee. These calling conventions affect the order of parameter passing, the cleanup of the stack after the function call, and other aspects related to function invocation.

Some of the scenarios where lambda functions should not be used are:

  1. When the function is too complex: While lambda functions are a concise way to define functions, they should not be used for functions that are too complex. In such cases, it is better to define a named function.
  2. When the function is used frequently: If a lambda function is used frequently in the code, it is better to define a named function. This will make the code more readable and easier to maintain.
  3. When the function is not reusable: Lambda functions are anonymous functions that are defined inline. They cannot be reused in other parts of the code. If the function is needed in multiple parts of the code, it is better to define a named function.

Suitable Scenarios of Lamda:

Lambda functions are suitable for scenarios where we need to define small and anonymous functions that can be used anywhere in the program. They are especially useful when used in conjunction with the standard library algorithms such as for_each, transform, and accumulate.

Example:

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
  std::vector<int> numbers = { 3, 5, 1, 7, 9, 2, 8, 6, 4 };
  
  std::cout << "Before sorting: ";
  for (auto num : numbers) {
    std::cout << num << " ";
  }
  std::cout << std::endl;
  
  // Sort the vector using a lambda function
  std::sort(numbers.begin(), numbers.end(), [](int a, int b) { return a < b; });
  
  std::cout << "After sorting: ";
  for (auto num : numbers) {
    std::cout << num << " ";
  }
  std::cout << std::endl;
  
  return 0;
} 
/*
Op => 
amit@DESKTOP-9LTOFUP:~/OmPracticeC++$ ./a.out
Before sorting: 3 5 1 7 9 2 8 6 4
After sorting: 1 2 3 4 5 6 7 8 9
*/        

In the above example, we have a vector of integers that we want to sort. We use the std::sort function from the <algorithm> header to do the sorting. The std::sort function takes three arguments: the beginning and end iterators of the range to sort, and a function object that compares two elements and returns true if the first element should come before the second element in the sorted sequence.

Instead of defining a separate function object to pass to std::sort, we use a lambda function to define the comparison function inline. The lambda function is enclosed in square brackets [] and takes two int arguments a and b. The body of the lambda function simply compares the two arguments and returns true if a should come before b.

The advantages of using a lambda function in this case are:

  • It allows us to define the comparison function inline, without having to define a separate function object.
  • It makes the code more concise and easier to read, since the comparison function is defined right where it is used.

The limitations of lambda functions are:

  • They cannot be used in contexts where a function pointer is required.
  • They cannot have a variable number of arguments.
  • They cannot have a default value for any of their parameters.


How to use the lambda function in recursive?

lambdas can be used in recursive functions in C++, but they need to be declared with the std::function type instead of using the auto keyword, as lambdas have a type that is not known until the lambda is created.

Here is an example of a recursive lambda function:

#include <iostream>
#include <functional>

int factorial(int n) {
    std::function<int(int)> recurse = [&](int x) {
        return (x == 0) ? 1 : x * recurse(x - 1);
    };
    return recurse(n);
}

int main() {
    std::cout << factorial(5) << std::endl;
    return 0;
}
// Op => 120        

In the above example std::function<int(int)> recurse is a declaration of a variable named recurse. It is an instance of the std::function class template, which is a general-purpose polymorphic function wrapper in C++.

The template parameter int(int) specifies the signature of the function that can be stored inside recurse. In this case, it is a function that takes an integer argument and returns an integer result.

The initialization of recurse is done using a lambda expression: [&](int x). The & indicates that the lambda function captures variables from the surrounding context by reference. The lambda takes an int argument named x.

Note that the lambda function is declared with the std::function type and assigned to the variable recurse. This allows the lambda function to be called recursively within itself.

BTW what is std::function ?

std::function is a general-purpose polymorphic function wrapper, which is a type-safe container for functions, function objects, and lambdas. It provides a way to store and pass around callable objects, including functions with different signatures, in a type-safe manner.

The std::function template takes a function signature as a template parameter, and provides an object that can be used to store any callable object that matches that signature. The syntax for creating a std::function object is similar to that of a function pointer, with the addition of the std::function keyword and the use of angle brackets to specify the function signature.

Here is an example that demonstrates the use of std::function:

#include <iostream>
#include <functional>

int add(int a, int b) {
    return a + b;
}

int main() {
    std::function<int(int, int)> func = add; // create a std::function object that stores the function 'add'
    std::cout << func(1, 2) << std::endl; // call the stored function object
    return 0;
}        

In the example above, we first define a function add that takes two int parameters and returns their sum. We then create a std::function object named func that stores the add function. Finally, we call the stored function object by passing it two int arguments, and output the result.

One of the advantages of using std::function over function pointers is that std::function is a type-safe container that ensures that the stored function object matches the specified signature. Additionally, std::function provides a convenient way to store and pass around callable objects, including function objects and lambdas, which cannot be stored in a function pointer.

Another advantage of std::function is that it supports polymorphism, which means that a std::function object can store a callable object that is derived from the specified signature. This allows for greater flexibility in writing generic code that works with different types of callable objects.

However, it is important to note that there is some overhead associated with using std::function, as it involves a type erasure technique that incurs a small runtime cost. Additionally, std::function may not be suitable for all performance-critical applications, and should be used judiciously.


Example to demonstrate how std::function supports polymorphism.

std::function supports polymorphism, which means you can use it to store objects of derived classes as if they were objects of the base class.

Here's an example:

#include <iostream>
#include <functional>

class Shape {
public:
    virtual void draw() = 0;
};

class Circle : public Shape {
public:
    void draw() {
        std::cout << "Drawing a circle" << std::endl;
    }
};

class Square : public Shape {
public:
    void draw() {
        std::cout << "Drawing a square" << std::endl;
    }
};

int main() {
    std::function<void(Shape*)> drawFunc;
    
    Circle c;
    Square s;
    
    drawFunc = [&](Shape* shape) { shape->draw(); };
    
    drawFunc(&c);
    drawFunc(&s);
    
    return 0;
}
/*
Op => 
amit@DESKTOP-9LTOFUP:~/OmPracticeC++$ ./a.out
Drawing a circle
Drawing a square
*/        

In this example, we define a base class Shape with a pure virtual function draw(). We also define two derived classes, Circle and Square, both of which implement the draw() function.

In main(), we declare a std::function object named drawFunc that takes a pointer to a Shape object as its argument and returns nothing (void). We then create a Circle object c and a Square object s.

We then use a lambda expression to assign a function to drawFunc. This function takes a Shape* argument and calls the draw() function on that object.

Finally, we call drawFunc with both c and s, which works because both Circle and Square are derived from Shape and thus can be passed as Shape* arguments to the drawFunc function.


Lambda functions are particularly useful in cases where we want to define a function object inline and don't want to define a separate function object just for that purpose. They are also useful in cases where we want to define a short, simple function that is used only once and doesn't need to be defined separately.


Thanks for reading till end. Please comment if you have any.

要查看或添加评论,请登录

Amit Nadiger的更多文章

  • Rust modules

    Rust modules

    Referance : Modules - Rust By Example Rust uses a module system to organize and manage code across multiple files and…

  • List of C++ 17 additions

    List of C++ 17 additions

    1. std::variant and std::optional std::variant: A type-safe union that can hold one of several types, useful for…

  • List of C++ 14 additions

    List of C++ 14 additions

    1. Generic lambdas Lambdas can use auto parameters to accept any type.

    6 条评论
  • Passing imp DS(vec,map,set) to function

    Passing imp DS(vec,map,set) to function

    In Rust, we can pass imp data structures such as , , and to functions in different ways, depending on whether you want…

  • Atomics in C++

    Atomics in C++

    The C++11 standard introduced the library, providing a way to perform operations on shared data without explicit…

    1 条评论
  • List of C++ 11 additions

    List of C++ 11 additions

    1. Smart Pointers Types: std::unique_ptr, std::shared_ptr, and std::weak_ptr.

    2 条评论
  • std::lock, std::trylock in C++

    std::lock, std::trylock in C++

    std::lock - cppreference.com Concurrency and synchronization are essential aspects of modern software development.

    3 条评论
  • std::unique_lock,lock_guard, & scoped_lock

    std::unique_lock,lock_guard, & scoped_lock

    C++11 introduced several locking mechanisms to simplify thread synchronization and prevent race conditions. Among them,…

  • Understanding of virtual & final in C++ 11

    Understanding of virtual & final in C++ 11

    C++ provides powerful object-oriented programming features such as polymorphism through virtual functions and control…

  • Importance of Linux kernal in AOSP

    Importance of Linux kernal in AOSP

    The Linux kernel serves as the foundational layer of the Android Open Source Project (AOSP), acting as the bridge…

    1 条评论

社区洞察

其他会员也浏览了