Lambda Functions

#programming #cpp

This text describes and elaborates on lambda functions, specifically in C++.

What is a lambda function

It is just syntactic sugar for an unnamed class.

auto f = [x](int y) { return x + y; };

is roughly (internally) :

struct __Lambda {
    int x;                  // captured variable stored as member

	__Lambda(int x_) : x(x_) {}

    int operator()(int y) const {
        return x + y;
    }
};

auto f = __Lambda(x);

So a lambda is just an object of a custom unnamed class.
Calling a lambda calls an internal operator.

The lifetime of the lambda itself follows normal rules of C++ object lifetimes.
If it goes out of scope, it is destroyed.

Capturing variables

Capture by value

int x = 10;
auto f = [x]() { return x; };

Here, x is copied by value.

The object of lambda, gets the copy of the then value of x, and the lifetime is same as the lifetime of the lambda.
The value ofx is, by default, const inside the lambda. x cannot be changed inside the function.

For it to be non-const, we need mutable lambdas.

[x]() mutable { x++; }  // modifies the COPY

Note that this modifies only the internal copy of the lambda, not the underlying value it picked up.

Capture by reference

int x = 10;
auto f = [&x]() { return x; };

Lambda stores a reference(pointer) to the original memory of x.

Lifetime of variable inside the variable is the same as lifetime of variable outside it

You must guarantee that the variable outlives the lambda, otherwise calling the lambda after the variable has gone out of scope is UB.

Default captures

Syntax Meaning
[=] capture everything by value
[&] capture everything by reference
auto f = [=]() { return x + y; };  // copies both  
auto g = [&]() { return x + y; };  // references both

You can override:

[=, &x]   // mostly value, x by reference  
[&, x]    // mostly reference, x by value

Examples and common bugs

Returning Lambdas

The lambda returned by any function will try to also return the variables it holds, just think of everything in the "Object" Model.
If the variable ceases to exist before the lambda can be called (especially if captured by reference), then UB.

Safe:

auto make_lambda() {
    int x = 10;
    return [x]() { return x; };  // OK (copied)
}

Dangerous:

auto make_lambda() {
    int x = 10;
    return [&x]() { return x; }; // UB
}

Since when the function call returns, x ceases to exist, the reference inside the lambda is invalid.

Capturing this from inside class

class A {
public:
    int x = 10;

    auto f() {
        return [this]() { return x; };
    }
};

This returns a copy of the pointer. Note that it points to the same underlying memory as that of the original object.
If we store the lambda and try to call it when the original object has ceased to exist, we get UB.

Safer alternative : Copying object

return [*this]() { return x; };

This actually copies the object (we de-referenced this, trying to pass object itself as parameter, by value)


Move capture

auto ptr = std::make_unique<int>(10);
auto f = [p = std::move(ptr)]() {return *p;};

This transfers ownership of ptr to p.
The ptr itself becomes null after the declaration of f

After this declaration of lambda with a move-only parameter, the entire lambda becomes move-only. This means it can no longer be copied.
{cpp} auto g = f fails immediately.
{cpp}auto g = std::move(f); works just fine.
Calling a lambda after it has been moved from is again an UB.
Returning a move-only lambda is fine, because we throw the same object around without creating copies.
A single move-only parameter makes the entire lambda move only, doesn't matter if there are other copyable parameters.

We can move out the lambda, if the lambda is of the type
{cpp}auto f = [p = std::make_unique<int>(10)]() mutable {return std::move(p);};
Essentially, we return the moved thing, so if we do
{cpp}unique_ptr<int> x = f(); // ownership transferred
And then
{cpp}unique_ptr<int> y = f(); This will return a nullptr, since the move-only object we are trying to move has already been moved

Lambdas can have static variables inside them that persist throughout the lifetime of the lambda (same as class)

Lambdas that capture by reference, if copied (the lambda itself is copied), the reference is also copied, the new lambda gets a copy of the reference. Same underlying object.

Powered by Forestry.md