logoalt Hacker News

sltkryesterday at 6:32 PM3 repliesview on HN

Lambda expressions in C++ are simply syntactic sugar for defining function objects (aka functors): structs that overload operator() so you can call them as functions. Once you realize this, their features and limitations become immediately clear.

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.


Replies

gpderettayesterday at 7:22 PM

>You cannot pass a lambda expression to a function that expects a function pointer (e.g. bool(*)(int) in this example)

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.

ueckeryesterday at 7:33 PM

Regarding function syntax in C++: GCC's C++ frontend does not support nested functions. But more importantly, even if it did, I do not think there would be any conflict at all. While lambdas are lowered to function objects with an unique anonymous type in C++, the semantics of a lambda that uses lvalue capture

  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.

wasmpersonyesterday at 8:01 PM

> lambda expressions can only be passed to template functions

> 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.

show 1 reply