By: Brian Sohr, Principal Software Developer
Beginning in C++11, support for lambda expressions has enabled the use of anonymous function objects, either for passing as parameters to functions, or for direct invocation. In subsequent versions of C++, this support has been extended. Here, we will just compare a handful of simple lambda expressions, highlighting some differences in supported parameter types over three increasingly evolved versions of the language (C++11, C++14, and C++20, in these examples).
Over the past decade or so, we have likely become accustomed to seeing usages resembling the following:
char testarr[] = { 'h', 'b', 'c', 'a', 'e', 'g', 'd', 'f' };
std::sort(testarr, testarr + 8, [](char i, char j) {return i > j; });
This simply passes the lambda expression to the std::sort algorithm and, as expected, if we print the array after the call we have the original data arranged in descending (reverse-lexigraphical) order, or:
h g f e d c b a
Similarly, and in order to make comparisons between lambda types easier in these examples, we can do the following equivalent code with the lambda named.
auto mycomp = [](char i, char j) { return i > j; };
char testarr[] = { 'h', 'b', 'c', 'a', 'e', 'g', 'd', 'f' };
std::sort(testarr, testarr + 8, mycomp);
This is typical of a simple C++11 lambda expression - only char types in this instance, or types convertible to char are allowed. So, this limited utility is exposed when we want to support a type like float. If we replace the testarr above with:
float testarr[] = { -1.0f, 1.0f, -0.5f, 0.75f, 3.1f, 5.2f, -30.1f, 65.0f };
The code still compiles, with the warning that the conversion from float to char comes at the risk of data loss. This can be seen manifesting itself when we examine the new output:
65 5.2 3.1 1 -0.5 0.75 -1 -30.1
The sort wasn't completely successful, due to the inherent problem converting a float type to a char. We could obviously write an equivalent lambda accepting floats, but that becomes increasingly tedious as we want to support further types. Beginning in C++14, support for auto typed parameters is provided, so the code above can become the following similar version, incorporating both array types, and adjusting the comparison lambda accordingly:
auto mycomp = [](auto i, auto j) { return i > j; }; // C++14 – auto type in param
char testarr[] = { 'h', 'b', 'c', 'a', 'e', 'g', 'd', 'f' };
float testarr2[] = { -1.0f, 1.0f, -0.5f, 0.75f, 3.1f, 5.2f, -30.1f, 65.0f };
std::sort(testarr, testarr + 8, mycomp);
std::sort(testarr2, testarr2 + 8, mycomp);
Upon execution, the two arrays are in the desired order:
h g f e d c b a
65 5.2 3.1 1 0.75 -0.5 -1 -30.1
One shortcoming of our new comparison lambda is that each type being auto allows me to invoke it directly with two types that may not be suitable for comparison in the intended context. To illustrate a partial remedy for this, we will consider another example (taken in excerpt from modernescpp.com).
auto sumDec = [](auto fir, decltype(fir) sec) { return fir + sec; }; // arbitrary, but convertible types (C++14)
...
std::cout << "sumDec(2000, 11): " << sumDec(2000, 11) << std::endl;
...
std::cout << "sumDec(true, 2010): " << sumDec(true, 2010) << std::endl;
Upon execution, we get the following output:
sumDec(2000, 11): 2011
sumDec(true, 2010): 2
Beginning with C++14, we can use decltype to convert the second parameter to the deduced type of the first parameter on the caller side. In the first invocation, nothing interesting is happening – the call is being made with two int arguments, so we get the straightforward result of 2011. In the second, an interesting side-effect is demonstrated - the value 2010 is converted to true, as the compiler has deduced the type of the first argument, and then converted the second to to bool as well, hence we are given the result 1 + 1, or 2. Reversing the order of the arguments in the call (sumTem(2010, true), in this case) inversely forces the ‘true’ boolean to be promoted to an int, and yields the unsurprising result of 2011.
If we want only to allow identical (that is, not solely convertible) types as parameters, beginning with C++20, we can use template lambdas. To illustrate, we can use a similar excerpt from the same example (again from modernescpp.com).
auto sumTem = []<typename T>(T fir, T sec) { return fir + sec; }; // arbitrary, but identical types (C++20)
...
std::cout << "sumTem(2000, 11): " << sumTem(2000, 11) << std::endl;
...
std::cout << "sumTem(true, 2010): " << sumTem(true, 2010) << std::endl; // ERROR
The first use is just fine. The types are identical, and we get the familiar, expected result of 2011. In the second invocation, however, although the types are convertible, the usage of non-identical types is disallowed by the compiler.
These are a few examples of how lambda usage has been extended over subsequent versions of the C++ language. The samples are quite simple, but hopefully demonstrate the subtle improvement in the versatility of lambda parameter support as the language has continued to evolve. A number of useful examples, as well as a further discussion, can be found at https://www.modernescpp.com/index.php/more-powerful-lambdas-with-c-20.