I always wondered why C++ only added lambdas, but observing WG21 for a while, I assume this is just a random walk in language design. (not that it is different in WG14)
The important feature of lambdas is that they are expressions, not that they lack a name. The advantage of function expressions is you can write the body of the function exactly at the place where it is used. With GCC nested functions you either have to write the body of the function before its first use or else write the declaration of the function twice.
This matters for long chains of continuation passing:
foo(arg1, arg2, [](){
// do some work
bar(arg3, arg4, [](){
// do some more work
baz(arg5, arg6, [](){
});
});
});
Compare to the following, where the control flow is all out of order: void cb(void){
// Do some work
void cb2(void){
// do some more work
void cb3(void){
}
baz(arg5, arg6, cb3);
}
bar(arg3, arg4, cb2);
}
foo(arg1, arg2, cb);But I usually prefer the later anyway, because the code usually is not as nested anyway and having a name is often helpful, and also because I find the nested code with lambdas also not too readable. Other languages have better syntax for chaining functions in this way, i.e. with lambdas I would like to write like this:
foo(arg1, arg2, _)
.(int(int x)) { ... }
.(int(int y)) { ... };
(edit: or something, I think I got it a bit wrong, but you get the idea)But I agree, sometimes lambdas are better so it would be good to have both.
(There is the classical hack to define lambdas using statement expressions and nested functions.)
It is also my main complaint about traditional math notation, They enjoy trying to make the notation as concise as possible, single letter symbols, embedded subparts, no names for anything. I understand that having super a tight concise notation is a massive benefit while in the flow state, thinking about the problem. But man it makes it rough for us casual math enjoyers coming in fresh to the topic.
C++ lambda/closures are a bit clunky because you have to specify if the captures are by reference or by value, and you're better of having a good idea of what you're doing.
[1] https://en.wikipedia.org/wiki/Closure_(computer_programming)
If you are interested, I explore the design space here. https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3654.pdf
For example, here is a typical use of a lambda expression to filter a vector of values:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6, 5};
int threshold = 5;
std::erase_if(v, [&](int i) { return i < threshold; });
// prints 5 9 6 5
for (int i : v) std::cout << i << '\n';
}
The lambda expression is essentially shorthand for: ...
int threshold = 5;
struct lambda_t {
int &threshold;
bool operator()(int i) {
return i < threshold;
}
};
std::erase_if(v, lambda_t{threshold});
...
You could always do this in C++. The added value of the lambda expression syntax is that the compiler generates the boilerplate, and generates a unique name for lambda_t.The important takeaway is that every lambda expression corresponds with a unique type that is _not_ a function type, but a class type. Consequently, lambda expressions can only be passed to template functions like std::erase_if, which are parameterized with the callback type.
You cannot pass a lambda expression to a function that expects a function pointer (e.g. bool(*)(int) in this example), and that's where they differ from GCC-style nested functions, which actually behave like functions. It also explains why lambda expressions don't need a trampoline.
As an aside, you _can_ pass lambdas to non-generic functions using a type-erasing wrapper like std::function, but std::function is itself a class type too, so that still doesn't allow you to convert it to a plain function pointer.
Finally, you can of course assign a name to a lambda expression value, using this common pattern:
auto greet = [](const char *name) { std::cout << "Hello " << name << "!\n"; }
greet("Alice");
greet("Bob");
(Note that `auto` is necessary here because there is no way to explicitly refer to the compiler-generated name for the lambda type.)This is the closest you can get to a local function definition in C++. Admittedly the syntax is a little odd. You might wonder why there wasn't some additional syntactic sugar to make the definition look more normal. I suspect that wasn't a random decision, but rather intentionally avoiding conflicts with existing language extensions like GCC's local function syntax.
To be pedantic, you can: as long as the lambda doesn't close over any local variable, the object will decay to a function pointer.
auto f = [&](int x) - > int { return x + z; };
is the same as GCC's nested function int f(int x) { return x + z; }
except that latter can be converted via a trampoline to a regular function pointer (and maybe the observable type). But you could do just the same with a lambda using a trampoline! In any case, there is no conflict, either this conversion is allowed and one needs some hack to make it work such as a trampoline or it is not.So in C++ you could simply lower such nested functions to lambdas and it would cause no confusion with GCC's nested functions at all, because from a user's point of view they would work identically.
> there is no way to explicitly refer to the compiler-generated name for the lambda type.
"Voldemort" types. While intellectually I get the explanation for why C++/Rust lambdas are like this, I still strongly dislike them. Occasionally being unable to even articulate what something is feels like a failure in language design.
C recently got type inference via the "auto" keyword and it seemed like almost immediately there was a proposal to add voldemort types to the language.
Anyway, I don't see it mentioned anywhere in this discussion, but imo the area where c++ lambdas shine is the way they interact with copy constructors and move constructors.