C++ Core Guidelines: Function Objects and Lambdas

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

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

Lambda

Function objects and lambdas

I said I want 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 for getting a deeper understanding of lambda expressions. 

I will make it short because my plan is to write about lambda expression.

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 state.

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);
}

 

Instances of the struct AddObj and the function addFunc are both callables.  I defined the struct AddObj just in place. That is the C++ compiler implicitly doing 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 it's environment and therefore has state, the corresponding struct AddObj gets a constructor for initialising its members. If the lambda expression caputures its argument by reference, so 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 emphasise 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 of 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 to 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 quite 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 not capture the variables by reference but copy the arguments. If you break the last statement you will get undefined behaviour.

Here is an example to undefined behaviour 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 definition of the lambda addLocal (3) 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 behaviour is that the function makeLambda returns a lambda expression with a reference to the local variable local.

Any guess what value the call add10(5) will have in line (5)? Here we are.

 LambdaCaptureReference

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

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

To be honest, I like this rule because it makes your code more robust. Why does 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

 

Conceptunally, you only want to initialise widget x. If it is initialised it should stay constant. This is a idea we can not express in C++. If widget x is used in a multithreading program, you have to synchronise it.

This synchronisation would not be necessary if widget x was constant. Here is the good 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 and ,therefore, you can use it in a multithreading program without expensive synchronisation.

What's next?

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

 

Thanks a lot to my Patreon Supporter: Eric Pederson.

 

Get your e-book at leanpub:

The C++ Standard Library

 

Concurrency With Modern C++

 

Get Both as one Bundle

cover   ConcurrencyCoverFrame   bundle
With C++11 and C++14 we got a lot of new C++ libraries. In addition, the existing ones are greatly improved. The key idea of my book is to give you the necessary information to the current C++ libraries in about 200 pages.  

C++11 is the first C++ standard that deals with concurrency. The story goes on with C++17 and will continue with C++20.

I'll give you a detailed insight in the current and the upcoming concurrency in C++. This insight includes the theory and a lot of practice with more the 100 source files.

 

Get my books "The C++ Standard Library" and "Concurrency with Modern C++" in a bundle.

In sum, you get more than 500 pages full of modern C++ and more than 100 source files presenting concurrency in practice.

 

Add comment


My Newest E-Books

Latest comments

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 900

All 538621

Currently are 203 guests and no members online