Modern C++: Lambdas - CYF Blog

Modern C++: Lambdas

STL

  • Data stuctures as ranges
  • Algorithm
  • Ilterators as glue interface
  • Callables

Before Modern C++:

class Person {
    public:
        std::string getName() const;
        std::string getId() const;
}
bool lessName(const Person& p1, const Person& p2) {
    return p1.getName() < p2.getName();
}
bool lessId(const Person& p1, const Person& p2) {
    return p1.getId() < p2.getId();
}

std::vector<Person> coll;

std::sort(coll.begin(), coll.end(), lessName);
std::sort(coll.begin(), coll.end(), lessId);

The problem is that we have to define functions, and functions have several drawback. One is that we cannot define function inside local scope. It’s hard to maintain the code due to naming.

More General Approach

Creater helper functions which is callable. One of possible solution is Lambda function

std::vector<Person> coll;

std::sort(coll.begin(), coll.end(), 
         [](const Person& p1, const Person& p2) {
             return p1.getName() < p2.getName;
         });
std::sort(coll.begin(), coll.end(), 
         [](const Person& p1, const Person& p2) {
             return p1.getId() < p2.getId;
         });

Lambda: function object defined on the fly. In C++14, we even skip the specific type to more general type

std::vector<Person> coll;

std::sort(coll.begin(), coll.end(), 
         [](const auto& p1, const auto& p2) {
             return p1.getName() < p2.getName;
         });
std::sort(coll.begin(), coll.end(), 
         [](const auto& p1, const auto& p2) {
             return p1.getId() < p2.getId;
         });
auto twice = [](const auto& x) {
    return x+x;
};

auto i = twice(3);
auto d = twice(1.7);
auto s = twice(std::string{"hi"});
auto t = twice("hi"); // Error: const char[3] + const char[3]

Lambdas as Better Functions

bool less7(int v) {
    return v < 7;
}

bool less8(int v) {
    return v < 8;
}

count_if(c.begin(), c.end(), less7);
count_if(c.begin(), c.end(), less8);

// what if the value is undetermined ?
void foo(int max) {
    count_if(c.begin(), c.end(), lessMax); // ?????
}

Lambdas can capture on the behavior parameters.

  • Functionality can depend on run-time parameters
void foo(int max) {
    count_if(c.begin(), c.end(),
    [max] (int v) {
        return v < max;
    }); 
}

Lambdas are function object

  • Lambdas are function objects
    • of a “unique non-union class closure type”
    • The object can be used like functions
      • Having operator() defined It has the same effect as:
auto add = [] (int x, int y) {
    return x + y;
};

class lambda??? {
    public:
    auto operator() (int x, int y) const {
        return x + y;
    }
}

auto add = lambda???{};
while (...) {
    int min, max;
    ...
    p = std::find_if(coll.begin(), coll.end(),
                    [min, max](int i) {
                        return min <= i && i <= max;
                    });
}

class lambda??? {
    private:
    int _min, _max;
    public:
    lambda???(int min, int max): _min(min), _max(max) {} // only callable before C++20
    auto operator() (int i) const {
        return min <= i && i <= max;
    }
};

// It is equal to
while (...) {
    int min, max;
    ...
    p = std::find_if(coll.begin(), coll.end(),
                    lambda???(min, max));
}

Type of Lambdas

  • Each lambda has its own (closure) type
    • The type of the lambda expression is a unqiue, unnamed nonunion class type
auto x = [] () {}; // x has its own type
auto y = [] () {}; // y has its own type

std::is_same<decltype(x), decltype(y)>::value; //yields false
auto z = x; // z has the same type
std::is_same<decltype(x), decltype(z)>::value; //yields true
  • Lambdas are objects of a generated class
    • The closure type is a class
  • You cannot find out whether an object is a lambda
// There is no std::is_lambda<> 
bool b1 = std::is_class<decltype(twice)>::value;  //true
bool b1 = std::is_class<decltype(twice)>::value;  //true (since C++17)

Generic Functions vs. Generic Lambdas

  • Generic Lambdas since C++14:
auto printLmbd = [] (const auto& coll) {
    for (const auto& elem: coll) {
        std::cout<< elem << '\n';
    }
};
  • Generic functions:
template<template T>
void printFunc(const T& coll) {
    for(const auto& elem: coll) {
        std:cout << elem << '\n';
    }
}
std::vector<int> v;
...
printFunc(v);
printLmbd(v);
printFunc<std::string>("hi");     //OK
printLmbd<std::string>("hi");     //ERROR
// To specify the template parameter use: printLmbd.operator()<std::string>("hi")
// The object don't have a template

call(printFunc, v);               // ERROR
call(printFunc<dectype(v)>, v);   //OK
call(printLmbd, v);               // OK, Lambda is not a generic object

Lambdas: Return Type

  • The minimal lambda:
[]{} // equivalent to []() -> void { }
  • The return type can be deduced
    • void without any return statement
    • auto if all return statements have same value
[] (long val) {        // ERROR: return types differ
    if (val < 10) {
        return val;
    }
    return 10;
}

[] (long val) -> long {        // OK
    if (val < 10) {
        return val;
    }
    return 10;
}

Lambdas as Function Pointers

  • Lambdas can be used as ordinary function pointers
    • No captures allowed
    • Can even passed to C functions
#include <cstdlib>    // for atexit()

void (*logFP)(const std::string& msg);    // actual logger (function pointer to log msg)

int main() 
{
    std::atexit([]() {                    // register lambda to call on program exit
        std::cout<<"good bye\n";
    });
    
    logFP = [](const auto& string msg) {  // set lambda as logger
        std::cout<< "LOG> "<<msg << '\n';
    };
     
    logFP("we are insdie main()");        //call the actual logger
}

Lambda: Caputre by Reference

std::vector<int> coll;

double sum = 0.0;
std::for_each(coll.begin(), coll.end(), [&sum](double d) { //[&] would catch all by reference
    sum+= d;
})

Other usages

  • Captures allow access to scope where lambda is defined
    • Global object are visible anyway
    • Object copied by-value are read-only(by default)
  • Examples:
    • [x, y]
      • access to a copy of x and y
    • [&]
      • access to all objects of outside scope by reference
    • [=]
      • access to a copy of all used objects
      • Avoid capturing with = to avoid expensive copies
    • [&x, y]
      • access to x by reference, but to a copy of y
    • [&,x]
      • access to a copy of x, but to all other objects by reference