Skip to main content

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.

L1

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.
TopicCore ideaTypical use
Lambda[capture](args){ body }sort, find_if, callbacks
Move / r-valuesT&&, std::moveAvoid expensive copies
RAIICtor acquires / dtor releasesFiles, 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 sort are 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.
CaptureMeaningWatch out
[]Capture nothingCannot use locals
[=]Used locals by valueSafe copy; may be stale
[&]Used locals by referenceDangling if lambda outlives scope
[x, &y]Explicit mixClearest style for reviews
mutableAllow changing by-value capturesDoes 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

  • mutable lets 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/vector is expensive; **moving** steals the buffer pointer instead.
  • Moves make returning containers and transferring ownership cheap.
CategoryMeaningExample
L-valueNamed, addressableint n;, s
R-valueTemporary / expiringn+1, string("hi")
CopyDuplicate resourcesstring b = a;
MoveSteal resourcesstring 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 from other, null it out.
  • Move assign: T& operator=(T&& other) noexcept — release old resource, then steal.
  • Mark moves noexcept when 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.
ResourceAcquire (ctor)Release (dtor)
Heap memorynew / make_uniquedelete / smart pointer dtor
Fileopen stream / fopenclose / stream dtor
Mutexlock_guard lockslock_guard unlocks
Socketconnect / acceptclose 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_shared are the preferred creators.
  • Moving a unique_ptr transfers 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_ptr by 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::move is 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_ptr for exclusive heap ownership.
  • No finally in C++ — RAII is the cleanup story.
L2

Advanced — Lambdas / Move / RAII

Lambdas and std::function

  • std::function type-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 this is 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::forward(x) preserves whether the argument was lvalue or rvalue.
  • 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).
ExpressionTypical result
T b = a; (a named)Copy (if available)
T b = std::move(a);Move
T b = factory();Move / elision from temporary
unique_ptr copyIll-formed — must move

RAII + exceptions

  • Stack unwinding destroys locals — RAII still releases resources.
  • Raw new without an owner can leak if something throws before delete.
  • 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::move something you still need.
  • Exclusive resources: delete copy, implement/allow move, or use unique_ptr.
  • API surface: accept callables as templates; store them as std::function only when needed.
L3

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.

L4

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;
}
L5

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.
L6

Practice Programs List

  1. Sort a vector of strings by length using a lambda comparator.
  2. Use count_if to count negatives; print the count.
  3. Capture a factor by value and scale every element via for_each.
  4. Write a mutable lambda counter and show the outer variable unchanged.
  5. Move a string and a vector; print sizes before/after.
  6. Implement a tiny RAII class that logs acquire/release.
  7. Transfer unique_ptr with std::move; assert source is null.
  8. Store two lambdas in vector> and run them.
  9. Init-capture a unique_ptr into a lambda and call it.
  10. Explain in comments why returning [&] captures is dangerous.
  11. List five STL types that follow RAII.
  12. Write a short note: when copy runs vs when move runs.