Sonkeng ::

Git Evangelist,C++ Alchemist,
Engineering Africa

:: C++ lambda: The basics


Posted on by sdmg15

If you have a Math background, you’ll have probably already heard about Lambda. In Programming in general and in C++ specifically, the concept may not resemble at all at what you think but don’t be afraid, in this post I’ll briefly introduce you to what Lambdas are and how to use and take advantage from them.

Before lambda: the earlier age …

The Standard Template Library (STL) is composed of containers. Iterating over them is quite easy, you can choose to use a build-in Algorithm like for_each or even use a raw for loop. But as a rule of thumb it’s recommended to use standard algorithms, because they express well the intend and make the code cleaner.

The for_each algorithm overload that we will use takes three parameters:

  • The first one is an iterator pointing at the beginning of the container

  • The second one is another iterator but that points at this time after the end of the container

  • The third one is a Callable

A Callable maybe a class or a struct that overrides the bracket operator (operator()), or a function pointer/reference.

Let’s enlight our words with the following example:

#include <iostream> 
  struct TestCallable {
    void operator() (int param){
       std::cout << param << "\n";
    }
  };

When passed as the third argument of for_each the operator will be called with the specified argument.

#include <algorithm>
#include <array>
#include <iostream>

struct TestCallable {
  void operator()(int param) { std::cout << param << "\n"; }

};

  auto main() -> int {
  std::array<int, 4> a = {1, 2, 3, 4};

  std::for_each(a.cbegin(), a.cend(), TestCallable());

  return 1;
}

Godbolt Link

The above snippet will print the elements of the container. This is what we call High Order Functions (HOF).

As you can see we were obliged to declare an external class and then overrides its bracket operator before being able to use it as a callback in the for_each algorithm. It maybe annoying having to do that everytime that we need a callback for doing a specific task.

Wait, you’re saying that everytime that I need a callback I’ll have to write such class? I’m abandoning here …

No don’t run out, I’m sure you have guessed that if I’m talking to you about that “problem” it’s because a solution actually exists, just keep scrolling :wink:.

Lambda to the rescue

Lambda is a feature introduce since C++11. It provides a concise way to create simple function object. The code we wrote earlier can be written like this using lambdas :


// Godbolt link : https://godbolt.org/z/Fr4utK

  #include <array>
  #include<iostream>
  #include <algorithm>

  auto main() -> int {
    std::array<int,4> a = {1,2,3,4};

    std::for_each( a.cbegin(),a.cend(), [](auto param){
        std::cout << param << "\n";
    });

    return 1;
  }

Godbolt Link

Note that we are not anymore using TestCallable as the third parameter. The two code will basically perform the same task, just that this one is using a lambda. Cleaner right?

Dude is that obscure syntax you want me to introduce in my code base?

Well, the lambda syntax can be obscure but once you’ve understood the meaning of each branches you will feel that your soul is now different from others :smile:

Depicting the different parts of a Lambda

Overall structure

A lambda is a closure type. It’s structured as follow since C++11: [](parameters) *specifier* -> *trailing return type* { *body* }

  • []: called the capture list or lambda introducer. It’s there that we will specify the variables that will be available in the Lambda scope. It helps introducing the lambda.

  • (parameters): Called the parameter list. It’s inside the brackets that we will specify the parameters that the lambda will take when called. You probably already know this from traditionnal functions. The brackets are optional.

  • specifier: It can be either constexpr or mutable, it’s an optional value.

  • Trailing return type: It’s an optoinal value, preceeded by an arrow -> which specifies that we are using the trailing return type syntax. When not specified the return type of the lambda will be deduced using template type deduction rules.

  • body: It’s where you write the content of your lambda.

With all that on hands, we can now write as much examples as we can:

Example One:


// Godbolt link https://godbolt.org/z/ysp-p_

#include <iostream>

auto main() -> int {
  auto sumLambda = [](int a,int b) -> int { return a + b; }; // Declaring about lambda
  auto sum = sumLambda(4,5); // Calling the lambda with 4 and 5 as parameters 
  std::cout<< "The sum is: "<< sum << "\n";
  return 1;
}

Godbolt Link

Example Two:

#include <iostream>

auto main() -> int {
  
  int age{20};
  auto changeAge = [](){ age = 10; std::cout << age << "\n"; };
  changeAge(); // Invoking the lambda at this point taking not parameters.

  return 1;
}

Godbolt Link

When writting such syntax, internally the compiler will generate a unique unnamed class that overrides the operator() just like we did before. The type of the lambda is only known at compile time, we need to use auto keyword so that the compiler will replace it by a type he generated.

The generated operator() by the compiler is by default const if we didn’t explicitly required it not to be by putting mutable in the specifier list.

To visualize how the compiler generates that I used CppInsights which helps us see what the compiler sees :eyes:. Here is the link to the First Example using cppinsights, https://cppinsights.io/s/3feb5a61. There you’ll see and understand more about what I was describing.

Okay cook but dude, your second example is not running what’s going on?

Oh well, even me I don’t know what’s wrong. Just kidding let’s move to the next section to understand what’s wrong.

Lambda captures list

In the lambda body, variables of the parent scope (the scope in which the lambda was declared) are not available. If you want to make them being available you’ll need to specify that in the capture list. We don’t specify them randomly, there are some rules to follow:

  • [=]: will capture all variables in the parent scope by copy.

  • [&]: will capture all variables in the parent scope by reference.

  • [&a,b]: will capture a by reference and b by value

  • [&a]: will capture a from the parent scope by reference. It’ll be the only variable captured in this case.

  • [a]: will capture a from the parent scope by copy. It’ll be the only variable captured in this case.

  • [&a,=]: will capture a by reference and others by copy.

You should note that any capture may appear only once, so you cannot [a,a] else the compiler will scream of pain. You also cannot do something like [&, &i](){}; because you are already capturing all by reference, so the second term is not necessary. Those are just few examples of misuses of the capture list that will leave your compiler unhappy.

So coming back to our Example 2 we can rewrite it accordinly:


#include <iostream>

auto main() -> int {
  
  int age{20};

  // Defining the lambda and capturing `age` by reference
  auto changeAge = [&age](){ age = 10; std::cout << age << "\n"; };
  changeAge(); // Invoking the lambda at this point taking not parameters.

  return 1;
}

Godbolt link

If you were to capture age by value, you’ll have a compilation error, untill you write mutable in the specifierpart of the lambda, remember, this is because the generated operator() is by default const and in the lambda body we are modifying the value of age. The code will look like this if you want to capture age by value:

#include <iostream>

auto main() -> int {
    int age{20};
    // Defining the lambda and capturing `age` by value
    auto changeAge = [age]() mutable { age = 10; std::cout << age << "\n"; };
    changeAge(); // Invoking the lambda at this point taking no parameters.
    return 1;
}

Godbolt Link

But if we were only reading the variable age without modifying it, the mutable here will be useless.

Note that when capturing by value, a copy of captured symbols are created in the lambda scope, so any modification of those symbols will be on their copy. If you want the modification to affect the original symbol you’ll probably need to capture by reference instead.

Default Capture list with initializers

When capturing variable you may decide to give them some default values. An example will be more illustrative:

#include <iostream>
#include <utility>

auto main() -> int {
  int fuel = 10;
  int time = 1000;

  auto changeFuelAndPrint = [fuel = 20, &ltime = std::as_const(time)]() {
    std::cout << "Fuel default value in lambda: " << fuel << "\n";
    std::cout << "Read only ltime: " << ltime << "\n";
  };
  changeFuelAndPrint();  // Will print 20

  return 1;
}

Godbolt Link

The variable fuel in the lambda scope will be initialized to 20. The variable ltime is captured by reference and initialized with a time, with the std::as_const utility, the variable ltime will be read-only in the lambda body. You can play with that by creating multiple examples.

I noticed that you declare lambda first after then calling it in another line, ca we do better?

Yes sure IIFE ( Immediately Invoked Function Expression)

IIFE and Lambda

Lambda expressions are closure objects. They can be immediately invoked by putting () right after their declaration.

#include <iostream>

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

  return 1;
}

Godbolt Link CppInsights Link

If your Lambda was taking parameters, you’ll need to specify them in the brackets.

#include <iostream>

auto main() -> int {
  [](int valOne, int valTwo) {
    int r = valOne + valTwo;
    std::cout << r << "\n";
  }(4, 2);  // Invoking the lambda immediately with parameters 4 and 2, it'll
            // print 6

  return 1;
}

Godbolt Link

The simplest lambda you can write


auto main() -> int {
    []{};
  return 1;
}

The code above actually compiles, I won’t argue about it you have all the necessary weapons to understand how it’s behaving :wink:

Where to use Lambda

There are really a lot of places where you can take advantage of lambdas:

  • You can use them as callbacks

  • They help in complex initialization scenarios

  • You can use them to perform some inner actions without actually interferring with variables in the parent scope

Lambda expressions come in a really handy way, but you just have not to abuse of them, still consider using traditionnal functions when necessary. If you don’t use them well you’ll fall under missed optimization opportunities.

Final thoughts

Ah, Lambda ! We came to our end !

Personnally I like the syntax of lambda syntax, it becomes intuitive after you understand how it works. As the C++ language evolve quickly, some additions were made to lambda such as templated lambda (C++20) it’s uncovered in this post.

Now that you have enough information about this mindblowing subject, I can now throw you in the nature to go and seek deep information on it :wave:

References

1 - https://eel.is/c++draft/expr.prim.lambda

2 - https://en.cppreference.com/w/cpp/language/lambda

3 - https://www.bfilipek.com/2019/02/lambdas-story-part1.html