Generic code with generic lambda expression

A short description of how generic or polymorphic lambda expressions are useful for code readability and maintainability.

Tutorial | Oct 17, 2019 | nextptr 

Introduction

A lambda expression that has at least one parameter of auto type is called a generic lambda expression:

auto l = [](auto x) {
   // ....  
};

l(2);  // int param
l(2.5); // double param

Generic lambda was introduced in the C++14 standard. Conceptually, a generic lambda is equivalent to a function object with a templatized function-call operator method:

struct LambdaClosureType {
  ...
  template<template-params>
  ret operator()(params) const { ... }
  ....
};

That means, more than one function-call operator methods can be instantiated for a generic lambda object depending on the argument types.

Motivation

As much as lambda expressions are invaluable in improving readability and reducing the LOC, still sometimes, it could be very tedious to use them with specific parameter types:

std::vector<std::vector<std::string>> coll;

auto it =
std::find_if(coll.begin(), coll.end(), 
 [](const std::vector<std::string>& vs) {
   return vs.size() > 1000;
});

Not only the above code is hard on eyes, but it would always require some efforts even if the type of the parameter is changed to another compatible type. For example, replacing std::vector<std::string> with std::list<std::string> in coll above would require a change in the lambda's parameter type.

Let's change the above code to use the generic lambda:

std::vector<std::vector<std::string>> coll;

auto it = 
std::find_if(coll.begin(), coll.end(), 
 [](const auto& c) {
    return c.size() > 1000;
});

The above code with generic lambda is far easier to live with because it is more readable and maintainable.

There are a few more benefits of generic lambda, which we explore in the next sections.

Capture-less Generic Lambda to Function Pointers

A non-generic lambda with an empty capture-list can be converted to a function pointer. A capture-less generic lambda is not an exception to this. Besides, it can be converted to more than one compatible function pointers:

void f(void(*fp)(int)) { /*...*/ }
void g(void(*fp)(double)) { /*...*/ }

auto op = [](auto x) {
    /// generic code for x
};

// use 'op' as a generic callback function pointer 
f(op);
g(op);

Generic Recursive Lambda

The generic lambda expression also opened up the possibility of having recursive lambda functions without using std::function. A lambda expression does not have a named specific type. Therefore, before C++14, a recursive lambda expression had to be wrapped in an std::function:

// power recursive function
std::function<int(int,int)> power;
power = [&power](int base, int exp) { 
 return exp==0 ? 1 : base*power(base, exp-1); 
};

std::cout << power(2,10); // 2^10 = 1024

A generic recursive lambda, on the other hand, can not only take generic parameters but also it does not require an std::function:

// works on any numeric 'base' type
auto power = [](auto self, auto base, int exp) -> decltype(base) {
  return exp==0 ? 1 : base*self(self, base, exp-1);
};

std::cout << power(power, 2,10); //2^10 = 1024
std::cout << power(power, 2.71828,10); //e^10 = 22026.3

The above generic lambda version of power is not any better from the readability standpoint. However, it can work on any numeric base type, it is free of std::function, and it doesn't need to capture.

Further Reading 📚

Proposal for Generic (Polymorphic) Lambda Expressions

Proposal for Generic (Polymorphic) Lambda Expressions (Revision 2)

Lambda expressions: cppreference