Lambda

C++ Core Guidelines: Function Objects and Lambdas

I can not think about modern C++ without lambda expressions. So my wrong assumption was that there are many rules for lambda expressions. Wrong! There are fewer than ten rules. But as ever I learned something new.

Here are the first four rules for lambda expressions (short lambdas).

Lambda

Function objects and lambdas

I said I wanted to write about lambda functions. Maybe you are surprised that the headline is called function objects and lambdas. If you know that lambdas are just function objects automatically created by the compiler, then this will not surprise you. If you don’t know, read the following section because knowing this magic helps a lot in getting a deeper understanding of lambda expressions. 

I will make it short because I plan to write about lambda expressions.

Lambda functions under the hood

First, a function object is an instance of a class, for which the call operator ( operator() ) is overloaded. This means that a function object is an object that behaves like a function. The main difference between a function and a function object is: a function object is an object and can, therefore, have stated.

Here is a simple example.

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

int main(){
    
    struct AddObj{
        int operator()(int a, int b) const { return a + b; }
    };
    
    AddObj addObj;
    addObj(3, 4) == addFunc(3, 4);
}

 

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (open)
  • "Generic Programming (Templates) with C++": October 2024
  • "Embedded Programming with Modern C++": October 2024
  • "Clean Code: Best Practices for Modern C++": March 2025
  • Do you want to stay informed: Subscribe.

     

    Instances of the struct AddObj and the function addFunc are both callables.  I defined the struct AddObj just in place. The C++ compiler implicitly does that if I use a lambda expression.

    Have a look.

    int addFunc(int a, int b){ return a + b; }
    
    int main(){
        
        auto addObj = [](int a, int b){ return a + b; };
        
        addObj(3, 4) == addFunc(3, 4);
        
    }
    

     

    That’s all! If the lambda expression captures its environment and therefore has state, the corresponding struct AddObj gets a constructor for initializing its members. If the lambda expression captures its argument by reference, so does the constructor. The same holds for capturing by value.

    With C++14, we have generic lambdas; therefore, you can define a lambda expression such as [](auto a, auto b){ return a + b; };. What does that mean for the call operator of AddObj? I assume you can already guess it. The call operator becomes a template. I want to emphasize it explicitly: a generic lambda is a function template

    I hope this section was not too concise. Let’s continue with the four rules.

    F.50: Use a lambda when a function won’t do (to capture local variables, or to write a local function)

    The difference in the usage of functions and lambda functions boils down to two points. 

    1. You can not overload lambdas.
    2. A lambda function can capture local variables.

    Here is a contrived example of the second point.

    #include <functional>
    
    std::function<int(int)> makeLambda(int a){    // (1)
        return [a](int b){ return a + b; };
    }
    
    int main(){
        
        auto add5 = makeLambda(5);                // (2)
        
        auto add10 = makeLambda(10);              // (3)
        
        add5(10) == add10(5);                     // (4)
        
    }
    

     

    The function makeLambda returns a lambda expression. The lambda expression takes an int and returns an int.  This is the type of the polymorph function wrapper std::function: std::function<int(int)>. (1). Invoking makeLambda(5) (2) creates a lambda expression that captures a, which is, in this case, 5. The same argumentation holds for makeLambda(10) (3); therefore add5(10) and add10(5) are 15 (4).

    The next two rules are explicitly dealing with capturing by reference. Both are pretty similar; therefore, I will present them together.

    F.52: Prefer capturing by reference in lambdas that will be used locally, including passed to algorithms, F.53: Avoid capturing by reference in lambdas that will be used nonlocally, including returned, stored on the heap, or passed to another thread

    For efficiency and correctness reasons, your lambda expression should capture its variables by reference if the lambda expression is locally used. Accordingly, if the lambda expression is not used locally, you should copy the arguments and not capture the variables by reference. If you break the last statement, you will get undefined behavior.

    Here is an example of undefined behavior with lambda expressions.

    // lambdaCaptureReference.cpp
    
    #include <functional>
    #include <iostream>
    
    std::function<int(int)> makeLambda(int a){
        int local = 2 * a;
        auto lam = [&local](int b){ return local + b; };           // 1
        std::cout << "lam(5): "<<  lam(5) << std::endl;            // 2
        return lam;
    }
    
    int main(){
      
      std::cout << std::endl;
      
      int local = 10;
        
      auto addLocal = [&local](int b){ return local + b; };        // 3
        
      auto add10 = makeLambda(5);
        
      std::cout << "addLocal(5): " << addLocal(5) << std::endl;    // 4
      std::cout << "add10(5): " << add10(5) << std::endl;          // 5
      
      std::cout << std::endl;
        
    }
    

     

    The lambda addLocal (3) definition and its usage (4) is fine. The same holds for the definition of the lambda expression lam (1) and its usage (2) inside the function. The undefined behavior is that the function makeLambda returns a lambda expression referencing the local variable local.

    And guess what value the call add10(5) will have inline (5)? Here we are.

     LambdaCaptureReference

    Each execution of the program gives a different result for the expression (5).

    ES.28: Use lambdas for complex initialization, especially of const variables

    I like this rule because it makes your code more robust. Why do the guidelines call the following program bad?

    widget x;   // should be const, but:
    for (auto i = 2; i <= N; ++i) {             // this could be some
        x += some_obj.do_something_with(i);  // arbitrarily long code
    }                                        // needed to initialize x
    // from here, x should be const, but we can't say so in code in this style
    

     

    Conceptually, you only want to initialize widget x. If it is initialized, it should stay constant. This is an idea we can not express in C++. If widget x is used in a multithreading program, you have to synchronize it.

    This synchronization would not be necessary if widget x were constant. Here is the excellent pendant with lambda expressions.

     

    const widget x = [&]{
        widget val;                                // assume that widget has a default constructor
        for (auto i = 2; i <= N; ++i) {            // this could be some
            val += some_obj.do_something_with(i);  // arbitrarily long code
        }                                          // needed to initialize x
        return val;
    }();
    

     

    Thanks to the in-place executed lambda, you can define the widget x as a constant. You can not change its value; therefore, you can use it in a multithreading program without expensive synchronization.

    What’s next?

    One of the critical characteristics of object orientation is inheritance. The C++ Core Guidelines have roughly 25 rules for class hierarchies. In the next post, I will write about the concepts of interfaces and implementations in class hierarchies.

     

     

    Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Modernes C++ GmbH

    Modernes C++ Mentoring (English)

    Do you want to stay informed about my mentoring programs? Subscribe Here

    Rainer Grimm
    Yalovastraße 20
    72108 Rottenburg

    Mobil: +49 176 5506 5086
    Mail: schulung@ModernesCpp.de
    Mentoring: www.ModernesCpp.org

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *