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
- Lambda basics
- Capture modes
- mutable and exception specification
- Generic lambdas (C++14)
- Practical patterns
- Common mistakes
- Performance tips
- 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
| Syntax | Meaning | Lifetime safety | Typical use |
|---|---|---|---|
[] | Nothing captured | Always safe | No outer state |
[=] | Default by value | Safer for deferred calls | Async, threads, stored callbacks |
[&] | Default by reference | Safe only if invoked before scope ends | Sync sort / find_if |
[x] | x by value | Safer for deferred | Cherry-pick copies |
[&y] | y by reference | Immediate use | In-place updates |
[=, &y] | Mostly value, y ref | Mind y’s lifetime | Mostly copy, one ref |
[&, x] | Mostly ref, x value | x is a snapshot | Mostly ref, one copy |
[this] | Current object pointer | Object must live | Member functions |
[*this] (C++17) | Copy of *this | Safer for threads | Pass work across threads |
[p = std::move(ptr)] | Init capture + move | Ownership transfer | unique_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 */ };
Related reading (internal links)
- C++ STL algorithms — sort, find, transform with lambdas
- C++
autoanddecltype - C++ move semantics
Keywords
C++ lambda, lambda expression, capture [=] [&], mutable, generic lambda, sort lambda, find_if, etc.
Summary
| Item | Syntax | Role |
|---|---|---|
| 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]() mutable | Mutate 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:
- Keep short logic in lambdas.
- Mind reference capture lifetimes.
- Large objects: reference or move.
- Pair with STL algorithms.
- Use for callbacks.
Related posts
- C++ lambda basics — capture, mutable, generic lambdas
- C++ vector basics
- C++
std::functionand function objects - C++ map and set
- C++ container selection