C++20 lambda expression and template syntax
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:
- Comment
(1)
indicates that structIsVector
, which is a template class, inherits publiclystd::false_type
by default. - Comment
(2)
indicates that the structIsVector
inherits publicly fromstd::true_type
if the template parameterT
to the struct is of typestd::vector<T>
. - Comment
(3)
indicates that within the lambda expression astatic_assert
to check during compile time, that the parameter passed to the lambda expression is indeed a vector by making use of theIsVector
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