Advanced C++
Lambdas / Move / RAII
Complete lesson for Lambdas / Move / RAII using LEARNCODEGURU’s 6-level learning model — from basics to advanced practice, with C++ OOP and STL where relevant.
Basics — Lambdas / Move / RAII
Section: Advanced C++. Modern C++ essentials — lambdas, r-values & move semantics, and RAII in depth.
What this lesson covers
- Three pillars of modern C++ that you will see in almost every professional codebase.
- **Lambdas** — short inline callables for algorithms and callbacks.
- **Move semantics** — cheap transfer of ownership instead of deep copies.
- **RAII** — bind resource lifetime to object lifetime for safe cleanup.
| Topic | Core idea | Typical use |
|---|---|---|
| Lambda | [capture](args){ body } | sort, find_if, callbacks |
| Move / r-values | T&&, std::move | Avoid expensive copies |
| RAII | Ctor acquires / dtor releases | Files, locks, smart pointers |
What is a lambda?
- A **lambda** is an anonymous **function object** written inline at the call site.
- Full shape:
[capture](parameters) mutable constexpr noexcept -> Ret { body }— most parts optional. - The compiler builds a unique closure type with
operator()for you. - Store it with
auto, or pass it straight into an algorithm. - **Real-life:** a sticky-note instruction — local, short, thrown away when done.
#include <iostream>
using namespace std;
int main() {
auto greet = []() { cout << "Hello\n"; };
auto add = [](int a, int b) { return a + b; };
greet();
cout << add(2, 3) << "\n"; // 5
return 0;
} Lambdas with STL algorithms
- Lambdas shine with
:sort,for_each,find_if,count_if,remove_if, … - Replace bulky hand-written loops with a clear predicate.
- Comparators for
sortare often one-line lambdas.
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;
int main() {
vector<int> v = {3, 1, 4, 1, 5};
sort(v.begin(), v.end(), [](int a, int b) { return a > b; });
for (int x : v) cout << x << " ";
cout << "\n";
vector<string> words = {"pear", "fig", "apple"};
sort(words.begin(), words.end(),
[](const string& a, const string& b) { return a.size() < b.size(); });
int n = count_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; });
cout << "evens=" << n << "\n";
return 0;
} Lambda captures
- The capture clause controls how the lambda sees enclosing locals.
[=]copies used locals;[&]binds them by reference (lifetime must outlive the lambda).- Mix defaults with specifics:
[=, &out]or[&, x]. - Inside member functions:
[this](pointer) or[*this](C++17 copy of object). - By-value captures are **const** inside the lambda unless you add
mutable.
| Capture | Meaning | Watch out |
|---|---|---|
[] | Capture nothing | Cannot use locals |
[=] | Used locals by value | Safe copy; may be stale |
[&] | Used locals by reference | Dangling if lambda outlives scope |
[x, &y] | Explicit mix | Clearest style for reviews |
mutable | Allow changing by-value captures | Does not change originals |
#include <iostream>
using namespace std;
int main() {
int factor = 10;
auto scale = [factor](int x) { return x * factor; };
auto bump = [&factor]() { factor += 1; };
bump();
cout << scale(3) << " factor=" << factor << "\n"; // 30 factor=11
int sum = 0;
auto addTo = [&sum](int x) { sum += x; };
addTo(5); addTo(7);
cout << "sum=" << sum << "\n"; // 12
return 0;
} mutable, return type, generic lambdas
mutablelets you modify **copies** captured by value.- Trailing return:
[](int x) -> double { return x * 0.5; } - C++14 **generic lambdas**:
[](auto a, auto b){ return a + b; }— like a mini template. - C++14 **init-capture**:
[p = std::move(up)]moves into the closure.
#include <iostream>
using namespace std;
int main() {
int n = 0;
auto tick = [n]() mutable { return ++n; }; // changes copy only
cout << tick() << " " << tick() << " outer=" << n << "\n"; // 1 2 outer=0
auto add = [](auto a, auto b) { return a + b; };
cout << add(2, 3) << " " << add(1.5, 2.5) << "\n";
return 0;
} L-values, r-values & why moves exist
- **L-value** — named object with identity (you can take its address).
- **R-value** — temporary / expiring value (
a+b, literal string object, returned temporary). - Copying a big
string/vectoris expensive; **moving** steals the buffer pointer instead. - Moves make returning containers and transferring ownership cheap.
| Category | Meaning | Example |
|---|---|---|
| L-value | Named, addressable | int n;, s |
| R-value | Temporary / expiring | n+1, string("hi") |
| Copy | Duplicate resources | string b = a; |
| Move | Steal resources | string b = std::move(a); |
std::move and r-value references
T&&is an **r-value reference** — binds to temporaries so overload resolution can pick a move.std::move(x)is a **cast** to r-value reference; it does not move by itself.- After a move, the source stays **valid but unspecified** (often empty) — do not assume old contents.
- Prefer moving when you are finished with the source object.
#include <iostream>
#include <string>
#include <utility>
#include <vector>
using namespace std;
int main() {
string a = "hello";
string b = std::move(a);
cout << "b=" << b << " a.size=" << a.size() << "\n";
vector<int> v1 = {1, 2, 3, 4, 5};
vector<int> v2 = std::move(v1);
cout << "v2.size=" << v2.size() << " v1.size=" << v1.size() << "\n";
return 0;
} Move constructor & move assignment
- Move ctor:
T(T&& other) noexcept— steal fromother, null it out. - Move assign:
T& operator=(T&& other) noexcept— release old resource, then steal. - Mark moves
noexceptwhen safe so containers (e.g.vector) can relocate efficiently. - **Rule of Zero:** prefer members that already manage memory (
string,vector,unique_ptr). - **Rule of Five:** if you define one of dtor/copy/move, review all five special members.
#include <utility>
class Buffer {
char* data = nullptr;
size_t n = 0;
public:
explicit Buffer(size_t sz) : data(new char[sz]), n(sz) {}
~Buffer() { delete[] data; }
Buffer(Buffer&& o) noexcept : data(o.data), n(o.n) {
o.data = nullptr;
o.n = 0;
}
Buffer& operator=(Buffer&& o) noexcept {
if (this != &o) {
delete[] data;
data = o.data;
n = o.n;
o.data = nullptr;
o.n = 0;
}
return *this;
}
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
}; What is RAII?
- **RAII** = Resource Acquisition Is Initialization.
- **Acquire** in the constructor; **release** in the destructor.
- Leaving scope (normal return **or** exception unwind) always runs the destructor.
- Fits files, locks, sockets, heap blocks, GPU handles, DB connections.
- **Real-life:** hotel key card — valid while checked in; invalid on checkout.
| Resource | Acquire (ctor) | Release (dtor) |
|---|---|---|
| Heap memory | new / make_unique | delete / smart pointer dtor |
| File | open stream / fopen | close / stream dtor |
| Mutex | lock_guard locks | lock_guard unlocks |
| Socket | connect / accept | close in dtor |
#include <cstdio>
class File {
FILE* fp = nullptr;
public:
explicit File(const char* path, const char* mode) {
fp = std::fopen(path, mode);
}
~File() {
if (fp) std::fclose(fp);
}
File(const File&) = delete;
File& operator=(const File&) = delete;
FILE* get() const { return fp; }
}; RAII with smart pointers
unique_ptr— exclusive ownership; movable, not copyable.shared_ptr— shared ownership with reference counting.make_unique/make_sharedare the preferred creators.- Moving a
unique_ptrtransfers ownership — classic RAII + move combo.
#include <iostream>
#include <memory>
#include <utility>
using namespace std;
int main() {
auto a = make_unique<int>(42);
unique_ptr<int> b = std::move(a);
cout << *b << "\n";
cout << (a == nullptr) << "\n"; // 1
return 0;
} How the three fit together
- RAII **owns** resources; **move** transfers ownership cheaply between RAII objects.
- Lambdas **capture** values/refs carefully — never
[&]a local into a long-lived callback. - Returning
vector/unique_ptrby value relies on moves (often elided too). - Init-capture
[p = std::move(up)]pulls unique ownership into a lambda.
Key points to remember
- Lambda:
[capture](args){ … }— default tool for STL predicates. std::moveis a cast; after move, treat source as empty-but-valid.- RAII: ctor acquires, dtor releases — prefer over manual
new/delete/fclose. - Prefer Rule of Zero; use
unique_ptrfor exclusive heap ownership. - No
finallyin C++ — RAII is the cleanup story.
Advanced — Lambdas / Move / RAII
Lambdas and std::function
std::functiontype-erases any callable with that signature.- Handy for storing callbacks in class members or containers.
- Slight overhead vs a template parameter that accepts the lambda directly.
#include <functional>
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<function<void()>> jobs;
jobs.push_back([]{ cout << "A\n"; });
jobs.push_back([]{ cout << "B\n"; });
for (auto& j : jobs) j();
return 0;
} Capture pitfalls & dangling references
- Never return a lambda that captured locals by
[&]— those locals die with the function. - Async / stored callbacks should capture by value or own data via
shared_ptr/ init-capture. - Capturing
thisis fine only while the object still lives.
// DANGEROUS pattern (do not use):
// auto bad() {
// int x = 10;
// return [&]{ return x; }; // x is gone!
// } Forwarding references (overview)
- In
template,void f(T&& x) T&&is a **forwarding** (universal) reference. std::forwardpreserves whether the argument was lvalue or rvalue.(x) - Used heavily in perfect-forwarding wrappers and emplacement APIs.
When copy runs vs when move runs
- Named objects prefer **copy** unless cast with
std::move(or they are temporaries). - Returning a local by value may use move or NRVO/copy elision.
- Deleted copy + available move ⇒ movable-only types (
unique_ptr, streams).
| Expression | Typical result |
|---|---|
T b = a; (a named) | Copy (if available) |
T b = std::move(a); | Move |
T b = factory(); | Move / elision from temporary |
unique_ptr copy | Ill-formed — must move |
RAII + exceptions
- Stack unwinding destroys locals — RAII still releases resources.
- Raw
newwithout an owner can leak if something throws beforedelete. - Never throw from a destructor during unwinding (can call
terminate).
#include <memory>
using namespace std;
void work() {
auto p = make_unique<int[]>(1000); // freed even if later code throws
// ...
} Design tips
- Keep lambdas short; extract a named function when the body grows.
- Do not
std::movesomething you still need. - Exclusive resources: delete copy, implement/allow move, or use
unique_ptr. - API surface: accept callables as templates; store them as
std::functiononly when needed.
Tricky Interview Points
Q1) What is a lambda in C++?
Answer An anonymous function object written as [capture](params){ body }, compiled into a unique closure type with operator().
Q2) Difference between [=] and [&]?
Answer [=] captures used locals by value; [&] captures them by reference. [&] risks dangling if the lambda outlives the locals.
Q3) What does mutable do on a lambda?
Answer It allows modifying by-value captures inside the lambda. It does not modify the original variables.
Q4) What is a generic lambda?
Answer A C++14 lambda with auto parameters, e.g. [](auto a, auto b){ return a + b; }.
Q5) What is an r-value reference?
Answer T&& binds to temporaries/expiring objects so move constructors and move assignments can run.
Q6) What does std::move do?
Answer It casts to an r-value reference so move overloads can be selected; it does not perform the move itself.
Q7) What is RAII?
Answer Resource Acquisition Is Initialization: acquire in the constructor and release in the destructor so object lifetime manages cleanup.
Q8) Give RAII examples in the standard library.
Answer fstream, vector, string, unique_ptr, shared_ptr, lock_guard, jthread (C++20).
Q9) How do moves and RAII relate?
Answer RAII types own resources; move operations transfer that ownership without deep copies.
Q10) Can lambdas capture by move?
Answer Yes via C++14 init-captures, e.g. [p = std::move(up)](){ ... }.
Q11) Why mark move operations noexcept?
Answer Containers like vector can move elements on reallocation only when moves are noexcept; otherwise they may copy for strong exception safety.
Q12) Is there finally in C++?
Answer No. Use RAII destructors for guaranteed cleanup.
Simple Programs
Lambda — count_if and sort
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> v = {1, 2, 3, 4, 5, 6};
int n = count_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; });
cout << n << "\n";
sort(v.begin(), v.end(), [](int a, int b) { return a > b; });
for (int x : v) cout << x << " ";
cout << "\n";
return 0;
} Lambda captures — sum by reference
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> v = {1, 2, 3, 4};
int sum = 0;
for_each(v.begin(), v.end(), [&sum](int x) { sum += x; });
cout << sum << "\n"; // 10
return 0;
} mutable lambda
#include <iostream>
using namespace std;
int main() {
int n = 0;
auto next = [n]() mutable { return ++n; };
cout << next() << " " << next() << "\n"; // 1 2
cout << "outer " << n << "\n"; // 0
return 0;
} std::move string and vector
#include <iostream>
#include <string>
#include <vector>
#include <utility>
using namespace std;
int main() {
string s = "LearnTechGuru";
string t = std::move(s);
cout << t << "\n";
vector<int> a = {1, 2, 3};
vector<int> b = std::move(a);
cout << b.size() << " " << a.size() << "\n";
return 0;
} RAII Trace class
#include <iostream>
using namespace std;
class Trace {
const char* name;
public:
explicit Trace(const char* n) : name(n) { cout << "acquire " << name << "\n"; }
~Trace() { cout << "release " << name << "\n"; }
};
int main() {
Trace a("A");
{
Trace b("B");
}
cout << "done\n";
return 0;
} unique_ptr — RAII + move
#include <iostream>
#include <memory>
#include <utility>
using namespace std;
int main() {
auto a = make_unique<int>(42);
unique_ptr<int> b = std::move(a);
cout << *b << "\n";
cout << (a == nullptr) << "\n";
return 0;
} std::function callback list
#include <iostream>
#include <functional>
#include <vector>
using namespace std;
int main() {
vector<function<int(int)>> ops;
ops.push_back([](int x) { return x + 1; });
ops.push_back([](int x) { return x * x; });
for (auto& f : ops) cout << f(3) << "\n"; // 4 then 9
return 0;
} Init-capture move into lambda (C++14)
#include <iostream>
#include <memory>
#include <utility>
using namespace std;
int main() {
auto up = make_unique<int>(7);
auto f = [p = std::move(up)]() { cout << *p << "\n"; };
f();
cout << (up == nullptr) << "\n";
return 0;
} Problem Solving Tricks
- Lambda: [capture](args){ body }.
[=]by value,[&]by reference — never dangle[&].- mutable changes the captured copy, not the original.
- Generic lambda: [](auto x){ ... } (C++14).
- std::move is a cast, not a mover by itself.
- After move, source is valid but usually empty.
- RAII: acquire in ctor, release in dtor.
- unique_ptr / vector / fstream are RAII by design.
- Prefer Rule of Zero when members already manage resources.
- Mark moves noexcept when they cannot throw.
- Init-capture [p = std::move(up)] owns inside the lambda.
- No finally — RAII is cleanup.
Practice Programs List
- Sort a vector of strings by length using a lambda comparator.
- Use count_if to count negatives; print the count.
- Capture a factor by value and scale every element via for_each.
- Write a mutable lambda counter and show the outer variable unchanged.
- Move a string and a vector; print sizes before/after.
- Implement a tiny RAII class that logs acquire/release.
- Transfer unique_ptr with std::move; assert source is null.
- Store two lambdas in vector
> and run them. - Init-capture a unique_ptr into a lambda and call it.
- Explain in comments why returning [&] captures is dangerous.
- List five STL types that follow RAII.
- Write a short note: when copy runs vs when move runs.