3.1.4 Generic Lambdas

What was the problem

The introduction of lambdas [2.1.7] in C++11 unlocked many possibilities for the programmer. Despite that, some use cases are still a bit difficult to tackle. Take for example a unique predicate that should be applied to two collections of different types, as in the example below:

#include <vector> 
#include <algorithm> 
 
bool same_count_by_sign 
(const std::vector<int>& ints, const std::vector<float>& floats) 
{ 
  if (ints.size() != floats.size()) 
    return false; 
 
  const auto non_positive_integer = 
    [](int v) -> bool 
    { 
      return v <= 0; 
    }; 
  const auto non_positive_float = 
    [](float v) -> bool 
    { 
      // This is the same body as above. Do I really need two lambdas? 
      return v <= 0; 
    }; 
 
  const std::size_t count_ints = 
    std::count_if(ints.begin(), ints.end(), non_positive_integer); 
  const std::size_t count_floats = 
    std::count_if(floats.begin(), floats.end(), non_positive_float); 
 
  return count_ints == count_floats; 
}

It is a bit disappointing to have two lambdas with the same body. Intuitively, one would want to templatize it. Unfortunately template lambdas do not exist, so we have to switch back to the pre-C++11 way with free functions or function objects.

#include <vector> 
#include <algorithm> 
 
namespace 
{ 
  // There we go, a simple predicate function declared far from its use. 
  template<typename T> 
  bool non_positive(T v) 
  { 
    return v <= 0; 
  } 
} 
 
bool same_count_by_sign 
(const std::vector<int>& ints, const std::vector<float>& floats) 
{ 
  if (ints.size() != floats.size()) 
    return false; 
 
  const std::size_t count_ints = 
    std::count_if(ints.begin(), ints.end(), &non_positive<int>); 
  const std::size_t count_floats = 
    std::count_if(floats.begin(), floats.end(), &non_positive<float>); 
 
  return count_ints == count_floats; 
}

It works, for sure, but it is not what we expect. Moreover, as soon as a variable has to be captured, we are back to the extremely verbose functor objects.

How the Problem is Solved

Enters C++14 and the generic lambdas. By declaring the lambda’s arguments as auto, we can declare what is effectively the equivalent of a template lambda.

#include <vector> 
#include <algorithm> 
 
bool same_count_by_sign 
(const std::vector<int>& ints, const std::vector<float>& floats) 
{ 
  if (ints.size() != floats.size()) 
    return false; 
 
  const auto non_positive = 
    [](auto v) -> bool 
    { 
      return v <= 0; 
    }; 
 
  // It’s the same predicate in both calls. 
  const std::size_t count_ints = 
    std::count_if(ints.begin(), ints.end(), non_positive); 
  const std::size_t count_floats = 
    std::count_if(floats.begin(), floats.end(), non_positive); 
 
  return count_ints == count_floats; 
}

We saw in 2.1.7 what was the equivalent function object of a lambda. Here the corresponding transformation, given a lambda declaration like

[](auto a1, , auto an ) -> /* return type */ { /* statements */ }

would be something like

struct something 
{ 
  template<typename T1, , typename Tn> 
  /* return type */ operator()(T1 a1, , Tn an) const { /* statements */ } 
};

Note that the type of the arguments are not related. Each use of auto here corresponds to a single template parameter. Also, it is totally possible to use declare a parameter pack [2.3.3], for example by using the auto… syntax.

Guideline

Mandatory auto-related notice: mind the next reader; write what you mean.

As usual with the auto keyword, one may be tempted to use it to not have to think about the actual needed type, or because it looks “generic” or “future proof”. Don’t do that.

The auto keyword is here to tell the compiler and the reader that the variable can be of many types. If we use it even when a single type is expected, it loses its meaning, and the code becomes harder to understand.