본문으로 건너뛰기 C++ Lambda Expressions | [=]·[&] capture, sort, find_if, and practical patterns

C++ Lambda Expressions | [=]·[&] capture, sort, find_if, and practical patterns

C++ Lambda Expressions | [=]·[&] capture, sort, find_if, and practical patterns

이 글의 핵심

C++ lambda expressions: [=]·[&] capture and using lambdas with sort and find_if. When functors feel heavy and real-world pitfalls show up—syntax, patterns, and fixes.

Introduction: Functors feel too heavy

“Do I need a class just to change the sort key?”

When sorting a vector, I needed a custom comparator. Writing a functor class every time felt like too much ceremony.

Common scenarios

Scenario 1: Passing a predicate to find_if

To find “the first person aged 25 or older,” std::find_if needs a predicate. A free function or functor at the top of the file drifts away from the call site and hurts readability. Scenario 2: Passing locals into a thread

std::thread takes a callable and arguments; combining several locals used to mean a struct or std::bind. With a lambda and an explicit capture list, you state what you move in and how in one place. Scenario 3: Callbacks that run later

For UI handlers or timer callbacks, passing by reference can dangle, while passing by value may not reflect updates. Understanding capture modes ([=], [&], [x, &y]) keeps this safe.

In plain terms, a lambda is syntax for a small, often anonymous function you define at the point of use—like a sticky note you throw away after one job. It shines wherever callbacks (passing “call me later” code) show up in the STL and APIs. If the lambda may outlive the surrounding scope, avoid reference captures unless lifetimes are guaranteed.

Watch reference capture [&]: If the lambda runs later (another thread, timer, stored in a container), [&] may capture locals that are already destroyed—undefined behavior (UB). For “runs immediately” callbacks (e.g. sort’s comparator), [&] is often fine; for “might run later,” prefer [=] or capture specific variables by value [x, y].

The problem code used a CompareByAge struct with operator() for std::sort. That is heavy when the rule is one-off, and every change to the key means editing a separate type. Lambdas put a nameless function at the call site so intent (e.g. sort Person by age) is obvious, and switching to name is a one-line body change. The third argument to sort is a function object that returns true if the first element should precede the second.

struct CompareByAge {
    bool operator()(const Person& a, const Person& b) const {
        return a.age < b.age;
    }
};
std::vector<Person> people = /* ....*/;
std::sort(people.begin(), people.end(), CompareByAge());

Lambda version:

// Runnable example
std::vector<Person> people = /* ....*/;
// Concise and clear
std::sort(people.begin(), people.end(),
    [](const Person& a, const Person& b) {
        return a.age < b.age;
    });

After reading this article you will:

  • Understand basic lambda syntax.
  • Use value vs reference capture correctly.
  • Apply lambdas effectively in real code.
  • Know performance characteristics and limitations.

A compact view of capture vs execution:

flowchart TB
  subgraph capture[Capture time]
    A[Lambda definition] --> B{Capture mode}
    B --> C["[=] by value"]
    B --> D["[&] by reference"]
    B --> E["[x, &y] mixed"]
  end
  subgraph exec[Call time]
    F[Lambda call] --> G{Captured kind}
    G --> H["Value: snapshot"]
    G --> I["Reference: live object"]
  end
  capture --> exec

Practical experience: This article draws on real issues and fixes from large C++ codebases—pitfalls and debugging tips you rarely see in textbooks.

Table of contents

  1. Lambda basics
  2. Capture modes
  3. mutable and exception specification
  4. Generic lambdas (C++14)
  5. Practical patterns
  6. Common mistakes
  7. Performance tips
  8. Production patterns

1. Lambda basics

Basic syntax

A lambda has capture [ ], parameters ( ), optional return type -> return_type, and body { }. Capture decides how outer variables are accessed; parameters and body resemble an ordinary function. The result is a callable object you can store in auto and invoke like a function pointer or functor.

// Basic shape
[capture](parameters) -> return_type {
    // body
}
// After pasting: g++ -std=c++17 -o lambda_basic lambda_basic.cpp && ./lambda_basic
#include <iostream>
int main() {
    auto add = [](int a, int b) -> int {
        return a + b;
    };
    int result = add(3, 5);  // 8
    std::cout << result << "\n";
    return 0;
}

Output: a single line 8.

Omitting the return type

If you omit -> return_type, the compiler deduces the return type from return statements. With a single return, omission is common; multiple returns must yield the same type.

// Deduced return type
auto add = [](int a, int b) {
    return a + b;  // int
};
auto multiply = [](double a, double b) {
    return a * b;  // double
};

Lambdas with no parameters

With no parameters you can write () or, since C++11, omit parentheses entirely: [] { ... } is a zero-argument lambda.

auto sayHello = []() {
    std::cout << "Hello!\n";
};
sayHello();  // Hello!
// Parentheses optional
auto sayWorld = [] {
    std::cout << "World!\n";
};

Immediately invoked lambdas

Define a lambda and call it immediately with (args) to run a one-off “local function.” Useful for complex initialization scoped to one block.

// Define and invoke immediately
int result = [](int x) {
    return x * x;
}(5);  // 25
// Initialization
auto data = []() {
    std::vector<int> vec;
    for (int i = 0; i < 10; ++i) {
        vec.push_back(i * i);
    }
    return vec;
}();

2. Capture modes

Capture by value [=]

[=] copies automatic variables used in the body at the point of definition. Inner x/y are snapshots; changing outer x later does not change what the lambda sees on invocation.

int x = 10;
int y = 20;
// Capture everything used by value
auto lambda = [=]() {
    std::cout << x << ", " << y << "\n";  // 10, 20
};
x = 100;  // Inner captured x unchanged
lambda();  // 10, 20

Capture by reference [&]

[&] captures automatic variables by reference. Mutations inside the lambda affect the originals, and reads see current values when the lambda runs—watch lifetime if invocation is deferred.

int x = 10;
int y = 20;
// Capture everything by reference
auto lambda = [&]() {
    x += 5;
    y += 10;
};
lambda();
std::cout << x << ", " << y << "\n";  // 15, 30

Explicit capture

List names explicitly: [x] is by value, [&y] by reference. Unlisted names are not accessible—clearer intent and fewer accidental copies or references.

int x = 10;
int y = 20;
int z = 30;
// x by value, y by reference, z not captured
auto lambda = [x, &y]() {
    std::cout << x << ", " << y << "\n";
    // std::cout << z;  // error: z not captured
};

Mixed default capture

[=, &y] defaults to by-value with y by reference; [&, x] defaults to by-reference with x by value.

int x = 10;
int y = 20;
int z = 30;
// Default by value, y by reference
auto lambda1 = [=, &y]() {
    std::cout << x << ", " << y << ", " << z << "\n";
};
// Default by reference, x by value
auto lambda2 = [&, x]() {
    std::cout << x << ", " << y << ", " << z << "\n";
};

Capturing this

Inside member functions, [this] lets the lambda use members. That follows this’s lifetime; if the lambda outlives the object, you get UB. Since C++17, [*this] copies the enclosing object.

class Counter {
    int count = 0;

public:
    void increment() {
        auto lambda = [this]() {
            count++;  // member access
        };

        lambda();
    }

    int getCount() const { return count; }
};

Capture cheat sheet

SyntaxMeaningLifetime safetyTypical use
[]Nothing capturedAlways safeNo outer state
[=]Default by valueSafer for deferred callsAsync, threads, stored callbacks
[&]Default by referenceSafe only if invoked before scope endsSync sort / find_if
[x]x by valueSafer for deferredCherry-pick copies
[&y]y by referenceImmediate useIn-place updates
[=, &y]Mostly value, y refMind y’s lifetimeMostly copy, one ref
[&, x]Mostly ref, x valuex is a snapshotMostly ref, one copy
[this]Current object pointerObject must liveMember functions
[*this] (C++17)Copy of *thisSafer for threadsPass work across threads
[p = std::move(ptr)]Init capture + moveOwnership transferunique_ptr, large handles

Init capture (C++14)

[name = expr] introduces a new name in the closure, initialized from expr. Use [p = std::move(ptr)] to move into the lambda without copying.

int x = 10;
auto lambda = [y = x + 5]() {
    std::cout << y << "\n";  // 15
};
auto ptr = std::make_unique<int>(42);
auto lambda2 = [p = std::move(ptr)]() {
    std::cout << *p << "\n";
};

3. mutable and exception specification

mutable lambdas

By-value captures are const inside operator() unless you add mutable, which lets you modify the copies stored in the closure—not the original outer variables.

int x = 0;
auto lambda1 = [x]() {
    // x++;  // error: const
    std::cout << x << "\n";
};
auto lambda2 = [x]() mutable {
    x++;  // OK: modifies the copy inside the closure
    std::cout << x << "\n";
};
lambda2();  // 1
lambda2();  // 2
std::cout << x << "\n";  // 0 (outer x unchanged)

noexcept

Marks the call operator as not throwing—required in some contexts; if an exception does escape, std::terminate may run.

auto lambda = []() noexcept {
    return 42;
};

Attributes

auto lambda = []() [[nodiscard]] {
    return 42;
};
// Warning if return value is ignored
lambda();

4. Generic lambdas (C++14)

auto parameters

C++14 allows auto parameters; each distinct argument type can instantiate a distinct closure type—handy for small generic helpers.

auto print = [](const auto& value) {
    std::cout << value << "\n";
};
print(42);
print(3.14);
print("hello");

Multiple generic parameters

auto add = [](auto a, auto b) {
    return a + b;
};
std::cout << add(1, 2) << "\n";
std::cout << add(1.5, 2.5) << "\n";
std::cout << add(std::string("Hello"), std::string(" World")) << "\n";

Template lambdas (C++20)

auto lambda = []<typename T>(T value) {
    std::cout << typeid(T).name() << ": " << value << "\n";
};
lambda(42);
lambda(3.14);

5. Practical patterns

Pattern 1: STL algorithms

Algorithms like find_if, count_if, all_of, and transform take predicates or unary operations—ideal lambdas.

Full example:

// g++ -std=c++17 -o lambda_stl lambda_stl.cpp && ./lambda_stl
#include <algorithm>
#include <iostream>
#include <vector>
struct Person {
    std::string name;
    int age;
};
int main() {
    std::vector<Person> people = {
        {"Alice", 25}, {"Bob", 30}, {"Charlie", 20}, {"Diana", 25}
    };
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    auto it = std::find_if(people.begin(), people.end(),
        [](const Person& p) { return p.age >= 25; });
    if (it != people.end())
        std::cout << "Found: " << it->name << ", " << it->age << "\n";
    int count = std::count_if(people.begin(), people.end(),
        [](const Person& p) { return p.age >= 25; });
    std::cout << "Count (age>=25): " << count << "\n";
    bool allAdult = std::all_of(people.begin(), people.end(),
        [](const Person& p) { return p.age >= 18; });
    std::cout << "All adult: " << (allAdult ? "yes" : "no") << "\n";
    std::vector<int> squares;
    std::transform(numbers.begin(), numbers.end(),
        std::back_inserter(squares),
        [](int x) { return x * x; });
    std::cout << "Squares: ";
    for (int s : squares) std::cout << s << " ";
    std::cout << "\n";
    return 0;
}

Sample output: Found: Alice, 25 / Count (age>=25): 3 / All adult: yes / Squares: 1 4 9 ... 100

std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto it = std::find_if(numbers.begin(), numbers.end(),
    [](int x) { return x % 2 == 0; });
int count = std::count_if(numbers.begin(), numbers.end(),
    [](int x) { return x % 2 == 0; });
bool allPositive = std::all_of(numbers.begin(), numbers.end(),
    [](int x) { return x > 0; });
std::vector<int> squares;
std::transform(numbers.begin(), numbers.end(),
    std::back_inserter(squares),
    [](int x) { return x * x; });

Pattern 2: Sorting

struct Person {
    std::string name;
    int age;
};
std::vector<Person> people = {
    {"Alice", 25},
    {"Bob", 30},
    {"Charlie", 20}
};
std::sort(people.begin(), people.end(),
    [](const Person& a, const Person& b) {
        return a.age < b.age;
    });
std::sort(people.begin(), people.end(),
    [](const Person& a, const Person& b) {
        return a.name.length() < b.name.length();
    });

Pattern 3: std::thread and lambdas

Prefer by-value capture of everything the thread needs so the parent stack frame can end safely.

// g++ -std=c++17 -pthread -o lambda_thread lambda_thread.cpp && ./lambda_thread
#include <iostream>
#include <thread>
#include <vector>
int main() {
    std::vector<int> data = {1, 2, 3, 4, 5};
    int multiplier = 10;
    std::thread t([data, multiplier]() {
        for (int x : data) {
            std::cout << x * multiplier << " ";
        }
        std::cout << "\n";
    });
    t.join();
    return 0;
}

Output: 10 20 30 40 50

Pattern 4: Callbacks

class Button {
    std::function<void()> onClick;

public:
    void setOnClick(std::function<void()> callback) {
        onClick = callback;
    }

    void click() {
        if (onClick) onClick();
    }
};
int main() {
    Button button;

    int clickCount = 0;
    button.setOnClick([&clickCount]() {
        clickCount++;
        std::cout << "Clicked " << clickCount << " times\n";
    });

    button.click();
    button.click();
}

Pattern 5: RAII helper (ScopeGuard)

template <typename Func>
class ScopeGuard {
    Func func;
    bool active = true;

public:
    ScopeGuard(Func f) : func(std::move(f)) {}

    ~ScopeGuard() {
        if (active) func();
    }

    void dismiss() { active = false; }
};
template <typename Func>
auto makeScopeGuard(Func func) {
    return ScopeGuard<Func>(std::move(func));
}
void processFile(const std::string& filename) {
    FILE* file = fopen(filename.c_str(), "r");
    if (!file) return;

    auto guard = makeScopeGuard([file]() {
        fclose(file);
        std::cout << "File closed\n";
    });
}

Pattern 6: Lazy evaluation

#include <iostream>
#include <optional>
#include <type_traits>

template <typename Func>
class Lazy {
    Func func;
    mutable std::optional<std::invoke_result_t<Func&>> cached;

public:
    Lazy(Func f) : func(std::move(f)) {}

    auto operator()() const {
        if (!cached) {
            cached = func();
        }
        return *cached;
    }
};
int main() {
    Lazy expensive([&]() {
        std::cout << "Computing...\n";
        return 42;
    });

    std::cout << expensive() << "\n";
    std::cout << expensive() << "\n";
}

Pattern 7: Recursive lambdas

Use std::function (C++14) or C++23 explicit object parameter.

#include <functional>
#include <iostream>

int main() {
    std::function<int(int)> factorial = [&](int n) {
        return n <= 1 ? 1 : n * factorial(n - 1);
    };
    std::cout << factorial(5) << "\n";  // 120

    // C++23 (when available)
    auto factorial2 = [](this auto self, int n) -> int {
        return n <= 1 ? 1 : n * self(n - 1);
    };
    std::cout << factorial2(5) << "\n";
    return 0;
}

6. Common mistakes

Error 1: Dangling reference

Symptoms: Crashes, garbage values, release-only bugs.
Cause: Reference-captured locals destroyed before the lambda runs.

std::function<void()> createBroken() {
    int x = 42;
    return [&x]() { std::cout << x << "\n"; };
}
std::function<void()> createSafe() {
    int x = 42;
    return [x]() { std::cout << x << "\n"; };
}

Error 2: Loop variable capture

Symptoms: Threads see the final index only.
Cause: [&i] shares one i across lambdas.

std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
    threads.emplace_back([&i]() {
        std::cout << i << "\n";
    });
}
for (int i = 0; i < 5; ++i) {
    threads.emplace_back([i]() {
        std::cout << i << "\n";
    });
}

Error 3: Mutating by-value capture without mutable

Symptoms: error: increment of read-only variable 'x'.

int x = 0;
auto bad = [x]() {
    // x++;
};
auto good = [x]() mutable {
    x++;
};

Error 4: std::function overhead

std::function may allocate. For hot paths, prefer template<typename F>.

std::function<void()> f = [x = 42]() { std::cout << x << "\n"; };

template <typename Func>
void run(Func&& f) { f(); }
run([]() { std::cout << "no heap\n"; });

Error 5: [this] after destruction

class Worker {
    void start() {
        std::thread t([this]() {
            this->doWork();
        });
        t.detach();
    }
};
class Worker : public std::enable_shared_from_this<Worker> {
    void start() {
        auto self = shared_from_this();
        std::thread t([self]() { self->doWork(); });
        t.detach();
    }
};

7. Performance tips

Tip 1: Minimize capture cost

  • Small types: cheap by-value [x].
  • Large objects: [&s] or init-capture with move [s = std::move(str)].
  • Avoid [=] if it copies unused bulky state.
std::string big(10000, 'x');
int threshold = 10;
auto bad = [=]() { return big.size() > threshold; };
auto good = [&big, threshold]() { return big.size() > threshold; };

Tip 2: Prefer templates over std::function

template <typename Compare>
void sortWithLambda(std::vector<int>& v, Compare cmp) {
    std::sort(v.begin(), v.end(), cmp);
}

Tip 3: noexcept when appropriate

auto safe = []() noexcept { return 42; };

Tip 4: IIFE for complex initialization

auto config = [&]() {
    Config c;
    c.loadFromFile("config.json");
    c.merge(defaults);
    return c;
}();

8. Production patterns

Pattern A: Threads and copied state

void processInBackground(const std::string& input) {
    std::thread t([input]() {
        auto result = expensiveComputation(input);
        saveResult(result);
    });
    t.detach();
}

Pattern B: std::async

auto future = std::async(std::launch::async, [data = prepareData()]() {
    return process(data);
});
auto result = future.get();

Pattern C: ScopeGuard with error paths

#include <fstream>
#include <filesystem>
void writeWithBackup(const std::string& path, const std::string& data) {
    std::string tmpPath = path + ".tmp";
    std::ofstream out(tmpPath);
    if (!out) throw std::runtime_error("Cannot open " + tmpPath);
    auto guard = makeScopeGuard([tmpPath]() {
        std::filesystem::remove(tmpPath);
    });
    out << data;
    out.close();
    std::filesystem::rename(tmpPath, path);
    guard.dismiss();
}

Pattern D: Strategy-style injection

template <typename OnSuccess, typename OnFailure>
void tryOperation(OnSuccess&& onOk, OnFailure&& onFail) {
    if (doSomething()) {
        onOk();
    } else {
        onFail();
    }
}
tryOperation(
    []() { log("OK"); commit(); },
    []() { log("Failed"); rollback(); }
);

Caveats

Dangling references

Returning a lambda that captures a local by reference is unsafe; prefer by-value for escaping closures.

std::function<void()> createLambda() {
    int x = 42;
    return [&x]() {
        std::cout << x << "\n";
    };
}
std::function<void()> createLambdaSafe() {
    int x = 42;
    return [x]() {
        std::cout << x << "\n";
    };
}

Capture cost

By-value capture of a large string copies eagerly. If you only read and the lambda does not outlive the scope, [&] avoids the copy; if you transfer ownership, use move init-capture.

std::string largeString(10000, 'x');
auto lambda1 = [largeString]() { /* copy */ };
auto lambda2 = [&largeString]() { /* reference */ };
auto lambda3 = [s = std::move(largeString)]() { /* moved */ };

  • C++ STL algorithms — sort, find, transform with lambdas
  • C++ auto and decltype
  • C++ move semantics

Keywords

C++ lambda, lambda expression, capture [=] [&], mutable, generic lambda, sort lambda, find_if, etc.

Summary

ItemSyntaxRole
By-value capture[=]Copy used automatic variables
By-reference capture[&]Reference used automatic variables
Explicit capture[x, &y]Mix per variable
this capture[this]Access members
Init capture[x = expr]New closure member
mutable[x]() mutableMutate by-value copies
Generic (C++14)[](auto ...)Polymorphic parameters

Capture selection:

  • Immediate use (sort, find_if): [&] or [=] usually fine.
  • Deferred (thread, async, stored callback): [=] or explicit by-value.
  • Large objects: [&] or [s = std::move(s)].

FAQ

Q. When do I use this in practice?

A. Full C++11 lambda guide: [=], [&], [this], mutable, C++14 generic lambdas, IIFE, sort, find_if, threads—apply the patterns and selection rules above.

Q. What should I read first?

A. Follow previous post links at the bottom of each article, or open the C++ series index.

Q. Where can I go deeper?

A. cppreference and official library docs; the links above are a good start.

One-line summary: Lambdas define small, local callables inline—ideal for STL algorithms and callbacks. Next: std::function and function objects (#13-2).

Previous: Practical C++ #12-3: optional, variant, any
Next: Practical C++ #13-2: std::function and function objects

Core principles:

  1. Keep short logic in lambdas.
  2. Mind reference capture lifetimes.
  3. Large objects: reference or move.
  4. Pair with STL algorithms.
  5. Use for callbacks.