This article intends to introduce “generic” lambdas, and also tries to explain how they are implemented so that a fuller understanding of how they work can be gained. So what is a generic lambda?
Consider the following code, and try to guess the output:
#include <iostream>
int main() {
auto g = [](auto n){ return n / 2; };
std::cout << "g(3.0): " << g(3.0) << '\n';
std::cout << "g(3): " << g(3) << '\n';
}
The syntax for the lambda g
is very similar to that seen already in the previous article, it is declared auto
with a pair of square brackets, followed by a parameter list and the lambda function body. The difference is that the parameter is declared auto
, too. This is highly intuitive, the coder says “Oh, the parameter type is auto-deduced, I suppose,” and they’re right. (This is now possible with free functions in C++20, too.)
Calling g(3.0)
(parameter type double
) means n
is a double
, so the return type is, too. Similarly, calling g(3)
(parameter type int
) means n is an int
, so the return type is int
, too.
(By the way, the return type is always auto-deduced from the lambda’s return
statement, you can’t specify it using a keyword other than auto
when defining the lambda, so int g = [](int n){ return n / 2; };
is wrong!)
So how does this work? In the above example program, the lambda g
is actually instantiated twice, once with a double
parameter, and again with an int
parameter. They may as well be two different functions. Seeing the word “instantiate” makes one think of C++ templates, and this would be thinking along the right lines.
Going back to C++98 we were introduced to function objects (sometimes called “functors”) as a way to utilize STL algorithms. These overloaded operator ()
(the “function call operator”) allowing them to be called (repeatedly) with each element of the container in turn (usually). Here is the above program rewritten in this style:
#include <iostream>
struct G {
template <typename T>
T operator()(T n) {
return n / 2;
}
};
int main() {
G g{};
std::cout << "g(3.0): " << g(3.0) << '\n';
std::cout << "g(3): " << g(3) << '\n';
}
I’m sure you’ll agree that lambda function syntax is far nicer and more convenient, however function objects still have some uses in Modern C++ and you may find them in legacy code, so knowledge of them is a good idea.
Going back to the return type deduction issue, it is possible to explicitly define this when using either a generic lambda or function object. With the lambda, simply create a variable of the desired type and return it, such as:
auto g = [](auto n){ double r = n / 2.0; return r; }; // return type is always double
Here, r
is guaranteed to be of type double
and there is no chance of integer division taking place, assuming that a valid type conversion is in fact possible (if not then a compile-time error is generated).
Function objects are even more flexible, for example:
#include <iostream>
template <typename U>
struct G {
template <typename T>
U operator()(T n) {
return n / 2.0;
}
};
int main() {
G<double> g{};
std::cout << "g(3.0): " << g(3.0) << '\n';
std::cout << "g(3): " << g(3) << '\n';
}
Notice that the return type is template parameter U
, and that this is specified when creating the object g
as in G<double> g{};
By contrast, writing the function object exactly as the lambda (notice that the return type of operator ()
is declared auto
) gives:
#include <iostream>
struct G {
template <typename T>
auto operator()(T n) {
double r = n / 2.0;
return r;
}
};
int main() {
G g{};
std::cout << "g(3.0): " << g(3.0) << '\n';
std::cout << "g(3): " << g(3) << '\n';
}
We’ve seen that generic lambdas are in fact function objects with easier syntax. (In fact, all lambdas can be rewritten by lowering them into function objects.) The lambdas we’ve seen so far have been stateless, this means they are pure functions (in the mathematical sense), acting only upon their parameter variable(s) to produce a single result. Stateful lambdas need to “capture” variables from their enclosing scope, and we’ll see how this is done in the next article of this mini-series.