C++20 lambda expression and template syntax

Gajendra Gulgulia
4 min readJul 25, 2021

Lambda expressions have been undoubtedly a celebrated addition to C++’s core language feature. Since its addition to C++ in the year 2011, it has gone through upgrades and evolution in the C++14 and C++17 standards. Today they are one of the most ubiquitous feature. No doubt they are first class citizens of C++ and still continue to evolve.

C++14 introduced the generic lambda expression into the core language feature which allowed the lambda expression to accept any parameter in its parameter list. C++20 has taken generic lambda expression to a next level whereby lambda expression allows template type parameter in its definition. The most simplest form of such lambda that captures nothing and does nothing looks like below:

[]<typename T>(){}

Lambda expression with template type parameter obviates the need for compile time checking of the captured or passed parameters in the body using SFINAE or type traits library. This allows us to write more expressive code and also generates better compiler errors. So lets explore the use cases of the new syntax for lambda expression and compare it with the way it was done before.

1. Lamda expressions and STL containers

1.1. The problem with generic lambda prior to C++20

Pre-C++20, the way to pass a STL container, say std::vector was using our favorite generic type auto

auto l1 = [](auto vec){
for(auto& i: vec){
//do something with each
//element in vec
}
};
int main(){
std::vector<int> vec{1,2,3,4,5,6,7,8,9};
l1(vec);
}

This construct seems simple and easy to use. But there is one pitfall to this: using auto means the parameter need not be std::vector<T> . One can example pass any STL container or a simple literal type:

int main(){
int one{1};
l1(one); //possible to do but causes error
}

To get rid of such errors, one may resort to using type trait like tricks or SFINAE. Consider the example below where we create an type trait like object that returns true when the template parameter is std::vector<T> else it returns false¹. Note the commented parts in the code

template <typename T>    //(1)
struct IsVector : std::false_type{};
template <typename T> //(2)
struct IsVector<std::vector<T>> : std::true_type { };

auto l2 = [](auto vec){
static_assert(IsVector<decltype(vec)>::value); //(3)
...
};

The above code snippet can be explained as follows:

  1. Comment (1) indicates that struct IsVector , which is a template class, inherits publicly std::false_type by default.
  2. Comment (2) indicates that the struct IsVector inherits publicly from std::true_type if the template parameter T to the struct is of type std::vector<T> .
  3. Comment (3) indicates that within the lambda expression a static_assert to check during compile time, that the parameter passed to the lambda expression is indeed a vector by making use of the IsVector object.

When the decltype(vec) is std::vector<T> , the object IsVector indicated in comment (2) gets materialized and consequently static_assert passes, allowing the lambda expression to evaluate the remaining code in its body. For all other parameters to the lambda expression, the IsVector indicated in comment (2) gets materialized causing static assert to fail.

1.2. The solution

With C++20 enabling to use template parameter with lambda expression, one can do this without typing extra code as in previous subsection

auto l3 = []<typename T>(std::vecto<T>& vec){ 
//do your stuff with std::vector<T>
};
int main(){
std::vector<int> vec1{1,2,3,4,5,6,7,8,9};
std::vector<float> vec2{1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7};
l3(vec1);
l3(vec2);
}

2. Perfect forwarding in lambda expressions

Before C++14 the way to perfect forward an argument passed to lambda expression was not possible without the use of decltype in the template argument of std::forward<T> . In the code snippet below, notice how the argument to lambda expression l1 is forwarded to myFunction


void myFunction(int&& myNum){
std::cout << "rvalue reference overload: myFunction(int&&)\n"
<< "myNum: << myNum << "\n";
}void myFunction(int& myNum){
std::cout << "lvalue reference overload: myFunction(int&)\n";
<< "myNum: << myNum << "\n";
}

auto l1= [](auto&& myNum){
myFunction(std::forward<decltype(myNum)>(myNum));
};
int main(){int someNum{121}; //lvalue
int& anotherNum = someNum; //lvalue reference

l1(someNum); //calls lvalue reference overload
l1(anotherNum); //calls lvalue reference overload
l1(2); //calls rvalue reference overload
l1(std::move(anotherNum)); //calls rvalue reference overload
}

With C++20, this syntax is obviated and one can simply forward the argument as is done for other methods where instead of auto type, the template type is used in the function parameter list:

auto l2 = []<typename T>(T&& myNum){
myFunction(std::forward<T>(myNum));
}
l2(someNum); //calls lvalue reference overload
l2(anotherNum); //calls lvalue reference overload
l2(2); //calls rvalue reference overload
l2(std::move(anotherNum)); //calls rvalue reference overload

Conclusion

The new addition to lambda expression in C++20’s core language feature will certainly help push towards writing cleaner and more expressive codes and getting better compiler errors. The template programming compatibility in generic lambdas also blend well with concepts and ranges.

References

The ideas for this article were motivated from the proposal paper: Familiar template syntax for generic lambdas

--

--