Lambdas and Move Semantics

Lambdas and Move Semantics

Use local callable objects and efficient value transfer in modern C++.

Lambdas and Move Semantics

Lambda basics

auto square = [](int x) {
    return x * x;
};

Capture lists

int factor = 2;
auto scale = [factor](int x) { return x * factor; };

Capture by value when the lambda should keep its own copy. Capture by reference only when the lambda really needs to observe or modify the surrounding object.

Moving values

std::string name = "Ada";
std::vector<std::string> names;
names.push_back(std::move(name));

After moving, the source object is valid but its exact value is unspecified.

That means you may destroy it, assign to it, or call operations whose post-move behavior is documented, but you should not rely on the old content still being there.

Main lessons

Capture carefully

int total = 0;
auto add = [&total](int value) { total += value; };

Init captures

auto fn = [name = std::string{"Ada"}] {
    std::cout << name << '\n';
};

Init captures are useful when a lambda should own moved data.

auto task = [buffer = std::vector<int>{1, 2, 3}] {
    return buffer.size();
};

This is often the cleanest way to transfer ownership into deferred work.

auto logger = std::make_unique<Logger>();
auto flush = [owner = std::move(logger)] {
    owner->flush();
};

This pattern matters because lambdas are often used to package work for later execution. Init-capture makes the ownership transfer explicit.

Moved-from objects

A moved-from object is valid but only safe to destroy, assign, or otherwise use according to its documented post-move guarantees.

Common pitfalls

Capturing by reference into longer-lived work

std::function<int()> make_bad_lambda() {
    int value = 42;
    return [&value] { return value; };
}

This returns a lambda holding a dangling reference. If the lambda may outlive the scope, capture by value instead.

Moving and then accidentally reading the source

std::string name = "Ada";
auto fn = [text = std::move(name)] {
    std::cout << text << '\n';
};

After this move, name still exists but should not be treated as if it still certainly contains "Ada".

Capturing a reference that outlives the scope

std::function<void()> bad_task() {
    std::string message = "ready";
    return [&message] {
        std::cout << message << '\n';
    };
}

This compiles, but the returned lambda keeps a dangling reference. If the lambda survives longer than the local scope, capture by value.

Practical patterns

Local callback for an algorithm

std::ranges::for_each(values, [](int& value) {
    value *= 2;
});

Moving ownership into deferred work

auto file = std::make_unique<Logger>();
auto flush = [owner = std::move(file)] {
    owner->flush();
};

Generic lambda

auto print_twice = [](const auto& value) {
    std::cout << value << ' ' << value << '\n';
};

Generic lambdas are often the fastest way to express a tiny template-like callback without writing a named function template.

Rule of thumb

Practical capture checklist

Example in practice

#include <algorithm>
#include <vector>

int main() {
    std::vector<int> values{1, 2, 3};
    std::for_each(values.begin(), values.end(), [](int& value) { value *= 2; });
    return values[0];
}

Try this variation

Capture a move-only resource into the lambda. That shows why lambdas are often the cleanest bridge between ownership and deferred work.

#include <memory>

int main() {
    auto value = std::make_unique<int>(7);
    auto task = [ptr = std::move(value)] { return *ptr; };
    return task();
}