Wednesday, October 7, 2026

C++ Threading interview questions

A 20-question C++ concurrency interview set, progressing from fundamentals to more advanced topics.

1. What is a race condition in C++?

A race condition occurs when multiple threads access shared data concurrently and the program's behavior depends on the timing of those accesses.

If multiple threads access the same memory location, at least one access is a write, and there is no proper synchronization, it can be a data race, which results in undefined behavior.

cpp

int counter = 0;


void increment() {

    ++counter; // Unsafe if called concurrently

}


Use a mutex:

cpp

std::mutex m;


void increment() {

    std::lock_guard<std::mutex> lock(m);

    ++counter;

}


Interview takeaway: A data race is not merely a logical bug—it is undefined behavior according to the C++ standard.


───


2. What is the difference between std::mutex and std::atomic?

std::mutex protects a critical section, while std::atomic provides atomic operations on an individual object.

cpp

std::atomic<int> counter{0};


counter.fetch_add(1);


A mutex:

cpp

std::mutex m;


{

    std::lock_guard<std::mutex> lock(m);

    ++counter;

}


Use an atomic when the operation you need can safely be expressed through atomic operations.

Use a mutex when you need to protect:

* Multiple variables

* A complex data structure

* An invariant involving several operations

Interview takeaway: Atomic doesn't mean "faster in every situation." Choose based on the synchronization problem you're solving.


───


3. What is a deadlock? How can you prevent it?

A deadlock occurs when threads wait indefinitely for resources held by each other.

For example:

text

Thread 1: locks A → waits for B

Thread 2: locks B → waits for A


One solution is consistent lock ordering.

A better option when locking multiple mutexes is:

cpp

std::scoped_lock lock(mutexA, mutexB);


Other prevention techniques:

* Always acquire locks in a consistent order.

* Keep critical sections small.

* Avoid unnecessary nested locking.

* Use RAII lock types.

* Use std::scoped_lock for multiple mutexes where appropriate.


───


4. What is the difference between std::lock_guard and std::unique_lock?

Both provide RAII-based mutex ownership.

lock_guard

Simple and lightweight:

cpp

std::lock_guard<std::mutex> lock(m);


It locks immediately and unlocks automatically when it goes out of scope.

unique_lock

More flexible:

cpp

std::unique_lock<std::mutex> lock(m);


It supports:

* Deferred locking

* Unlocking and relocking

* Moving ownership

* Conditional locking

* std::condition_variable

For example:

cpp

std::unique_lock<std::mutex> lock(m);

cv.wait(lock);


Interview takeaway: Use lock_guard for simple locking; use unique_lock when you need additional control or a condition variable.


───


5. What is a condition variable?

A std::condition_variable allows a thread to efficiently wait until some condition becomes true.

Example:

cpp

std::unique_lock<std::mutex> lock(m);


cv.wait(lock, [] {

    return !queue.empty();

});


Another thread can notify it:

cpp

{

    std::lock_guard<std::mutex> lock(m);

    queue.push(value);

}


cv.notify_one();


While waiting, the condition variable temporarily releases the mutex, allowing another thread to acquire it.

Interview takeaway: Condition variables are commonly used to implement producer-consumer queues.


───


6. What is a spurious wakeup?

A spurious wakeup means a thread waiting on a condition variable can wake up even though the desired condition isn't true.

Therefore, this is unsafe:

cpp

cv.wait(lock);


use(queue.front());


Instead:

cpp

cv.wait(lock, [] {

    return !queue.empty();

});


Conceptually, the second form does:

cpp

while (!queue.empty()) {

    // ...

}


More accurately:

cpp

while (!condition) {

    cv.wait(lock);

}


Interview takeaway: Always use a predicate with wait() unless you have a specific reason not to.


───


7. What is the difference between std::async, std::thread, and std::jthread?

std::thread

Represents a thread directly:

cpp

std::thread t([] {

    do_work();

});


t.join();


You manage its lifetime yourself.

std::async

Runs an asynchronous task and provides a future:

cpp

auto f = std::async(std::launch::async, [] {

    return 42;

});


int result = f.get();


std::jthread

Introduced in C++20. It provides RAII thread management and cooperative cancellation.

cpp

std::jthread t([] {

    do_work();

});


A jthread automatically joins when destroyed.


Feature

thread

async

jthread


Represents thread

Yes

Not directly

Yes


Returns result

No

Yes

No


Auto-join

No

N/A

Yes


Cancellation support

Manual

No direct mechanism

stop_token




───


8. What are std::future and std::promise?

They provide a mechanism for transferring a result from one thread to another.

Producer:

cpp

std::promise<int> promise;

auto future = promise.get_future();


std::thread t([&promise] {

    promise.set_value(42);

});


Consumer:

cpp

int result = future.get();


Conceptually:

text

Thread A

   |

   | set_value(42)

   v

promise

   |

   v

future

   |

   v

Thread B

   |

   | get()

   v

  42


They can also transfer exceptions.


───


9. What is the C++ memory model and what does "happens-before" mean?

The C++ memory model defines how threads interact with shared memory and what ordering guarantees synchronization operations provide.

A happens-before relationship establishes that one operation is ordered before another in the C++ memory model.

For example:

cpp

// Thread 1

{

    std::lock_guard lock(m);

    data = 42;

}


// Thread 2

{

    std::lock_guard lock(m);

    std::cout << data;

}


The unlock performed by Thread 1 synchronizes appropriately with a subsequent lock acquired by Thread 2, so Thread 2 can safely observe the protected state.

Interview takeaway: Understanding happens-before is essential for understanding why mutexes and atomics make communication between threads safe.


───


10. What are C++ atomic memory orders?

Common memory orders are:

cpp

std::memory_order_relaxed

std::memory_order_acquire

std::memory_order_release

std::memory_order_acq_rel

std::memory_order_seq_cst


Relaxed

Provides atomicity but minimal ordering guarantees.

Acquire

Ensures appropriate subsequent operations aren't moved before the acquire.

Release

Ensures appropriate preceding operations aren't moved after the release.

Acquire-release

Combines both.

Sequentially consistent

The strongest commonly used ordering and the default for many atomic operations.

Example:

cpp

std::atomic<bool> ready{false};

int data = 0;


// Producer

data = 42;

ready.store(true, std::memory_order_release);


// Consumer

if (ready.load(std::memory_order_acquire)) {

    std::cout << data;

}


The release/acquire pair establishes the required synchronization.


───


Advanced C++ Concurrency

11. What is false sharing?

False sharing occurs when different threads modify different variables that happen to reside on the same CPU cache line.

For example:

cpp

struct Data {

    std::atomic<int> a;

    std::atomic<int> b;

};


Thread 1 repeatedly updates a, while Thread 2 repeatedly updates b.

Although they don't logically share data, the CPU cache line containing both variables may continually bounce between cores.

This can severely hurt performance.

A common solution is to separate frequently modified variables onto different cache lines, using techniques such as padding or:

cpp

alignas(std::hardware_destructive_interference_size)

std::atomic<int> a;


Interview takeaway: False sharing is a performance problem, not normally a correctness problem.


───


12. What is lock-free programming?

Lock-free programming uses atomic operations instead of traditional mutex-based locking.

Example:

cpp

std::atomic<int> counter{0};


counter.fetch_add(1, std::memory_order_relaxed);


The goal is to allow threads to make progress without blocking on a mutex.

Important distinction:

* Lock-free: at least one thread can make progress.

* Wait-free: every thread can complete its operation within a bounded number of steps.

* Obstruction-free: a thread can make progress if it executes in isolation.

Interview takeaway: Lock-free does not automatically mean faster or easier to write correctly.


───


13. What is std::call_once used for?

std::call_once ensures that a particular initialization function executes only once, even when multiple threads call it concurrently.

cpp

std::once_flag flag;


void initialize() {

    std::call_once(flag, [] {

        // Initialization happens once

    });

}


Even if 100 threads call initialize(), the initialization code executes only once.

It's useful for thread-safe one-time initialization.

Interview takeaway: In modern C++, a function-local static is also thread-safe for initialization:

cpp

Singleton& instance() {

    static Singleton s;

    return s;

}



───


14. How does a thread pool work?

A thread pool maintains a fixed number of worker threads and a queue of tasks.

Conceptually:

text

+----------------+

submit() ---> |   Task Queue   |

              +-------+--------+

                      |

          +-----------+-----------+

          |           |           |

       Worker 1    Worker 2    Worker 3


Typical implementation:

1. Create N worker threads.

2. Store tasks in a synchronized queue.

3. Workers wait on a condition variable.

4. When a task arrives, notify a worker.

5. Worker removes and executes the task.

6. Repeat until shutdown.

The main advantage is avoiding the overhead of repeatedly creating and destroying threads.


───


15. How would you implement a thread-safe queue?

A simple mutex + condition-variable implementation looks like:

cpp

template<typename T>

class ThreadSafeQueue {

    std::queue<T> queue;

    mutable std::mutex mutex;

    std::condition_variable cv;


public:

    void push(T value) {

        {

            std::lock_guard<std::mutex> lock(mutex);

            queue.push(std::move(value));

        }


        cv.notify_one();

    }


    T pop() {

        std::unique_lock<std::mutex> lock(mutex);


        cv.wait(lock, [this] {

            return !queue.empty();

        });


        T value = std::move(queue.front());

        queue.pop();


        return value;

    }

};


Important considerations in production code include:

* Shutdown handling

* Exceptions

* Move semantics

* Returning std::optional<T> where appropriate

* Avoiding unnecessary copies

* Supporting cancellation

Interview takeaway: The mutex protects the queue; the condition variable prevents workers from busy-waiting.


───


16. What is the ABA problem?

The ABA problem commonly appears in lock-free algorithms using compare-and-exchange.

Imagine:

text

Initial: A


Thread 1 reads A

              ↓

Thread 2 changes A → B

              ↓

Thread 2 changes B → A

              ↓

Thread 1 performs CAS expecting A


Thread 1 sees A again and assumes nothing changed.

But the value actually changed:

text

A → B → A


This can cause incorrect behavior in certain lock-free algorithms.

Possible solutions include:

* Version/tag counters

* Tagged pointers

* Hazard pointers

* Epoch-based reclamation

* Other safe memory-reclamation techniques

Interview takeaway: ABA is particularly important when implementing lock-free data structures involving pointer manipulation and memory reclamation.


───


17. What is the difference between notify_one() and notify_all()?

Given:

cpp

cv.notify_one();


Only one waiting thread is notified.

With:

cpp

cv.notify_all();


All waiting threads are notified.

Example:

text

Condition Variable

                  /      |      \

              Thread A Thread B Thread C


notify_one()  → wakes one

notify_all()  → wakes all


Use notify_one() when only one worker needs to proceed.

Use notify_all() when multiple waiting threads may need to re-check the condition.

Remember that being notified doesn't guarantee the thread will proceed—it must reacquire the mutex and re-check the predicate.


───


18. What happens if a std::thread is destroyed while still joinable?

This is a very common interview trap.

If a std::thread object is destroyed while joinable() == true, its destructor calls:

cpp

std::terminate();


Example:

cpp

void foo() {

    std::thread t([] {

        // work

    });


} // std::terminate()!


You must either:

cpp

t.join();


or:

cpp

t.detach();


before destruction.

With C++20, std::jthread is safer because it automatically joins when destroyed.


───


19. What is std::shared_mutex and how is it different from std::mutex?

std::shared_mutex allows multiple readers or one writer.

text

Readers:

R1 ─┐

R2 ─┼── Can execute concurrently

R3 ─┘


Writer:

W  ─── Exclusive access


Example:

cpp

std::shared_mutex mutex;


void read() {

    std::shared_lock lock(mutex);

    // Read shared data

}


void write() {

    std::unique_lock lock(mutex);

    // Modify shared data

}


With std::mutex, only one thread can hold the mutex regardless of whether it is reading or writing.

Use case: Data that is read frequently but modified relatively infrequently.


───


20. Explain a producer-consumer implementation using condition_variable.

This is one of the most important practical concurrency interview questions.

The producer adds work to a queue:

cpp

{

    std::lock_guard<std::mutex> lock(mutex);

    queue.push(item);

}


cv.notify_one();


The consumer waits until work is available:

cpp

std::unique_lock<std::mutex> lock(mutex);


cv.wait(lock, [] {

    return !queue.empty();

});


auto item = queue.front();

queue.pop();


Conceptually:

text

Producer

   |

   | push()

   v

+----------------+

| Shared Queue   |

+----------------+

        |

        | notify

        v

+----------------+

| Consumer       |

| wait()         |

+----------------+


The important points are:

1. The queue is protected by a mutex.

2. The consumer sleeps rather than continuously polling.

3. The condition variable wakes the consumer.

4. The consumer checks the condition again after waking.

5. The mutex protects both checking and modifying the queue.


───

For a senior C++ interview, I'd expect follow-up questions around memory ordering, atomics, lock-free data structures, cache coherence/false sharing, thread pools, and designing a thread-safe queue rather than just API definitions.

Monday, October 5, 2026

C++ template interview cheat sheet

 Here’s a C++ template interview cheat sheet with each question followed by an interview-ready answer, explanation, and example.


1. What is a template in C++? Why do we use templates?

Answer:

 A template allows us to write generic code that works with different data types without duplicating the implementation.

There are two main types:

* Function templates

* Class templates

Example

cpp

template <typename T>

T add(T a, T b)

{

    return a + b;

}


Now the same function can work with multiple types:

cpp

add(10, 20);          // int

add(2.5, 3.5);        // double

add(std::string("A"), "B");  // string


The compiler generates the appropriate version when the template is instantiated.

Why use templates?

* Code reuse

* Type safety

* Compile-time polymorphism

* Generic programming

* Often zero runtime overhead

Interview point: Templates are primarily a mechanism for compile-time polymorphism, whereas virtual functions provide runtime polymorphism.


───


2. What's the difference between typename and class in templates?

For declaring a template type parameter, they are generally equivalent:

cpp

template <typename T>

class Box {};


and:

cpp

template <class T>

class Box {};


Both mean that T is a type.

But typename has another important use

Consider:

cpp

template <typename T>

void foo()

{

    T::value_type x;

}


This is problematic because the compiler doesn't initially know whether T::value_type is a type or something else.

We tell the compiler explicitly:

cpp

template <typename T>

void foo()

{

    typename T::value_type x;

}


Here, typename means:

T::value_type is a type.

Interview trap

Don't say "typename and class are always interchangeable."

They're interchangeable when declaring a template type parameter, but typename has additional meaning for dependent names.


───


3. What is template specialization?

Specialization allows us to provide a different implementation for a particular type or category of types.

There are two important forms.

Full specialization

cpp

template <typename T>

class Printer

{

public:

    void print()

    {

        std::cout << "Generic\n";

    }

};


For int, we can provide a completely different implementation:

cpp

template <>

class Printer<int>

{

public:

    void print()

    {

        std::cout << "Integer\n";

    }

};


Now:

cpp

Printer<double> p1;

p1.print();       // Generic


Printer<int> p2;

p2.print();       // Integer


Partial specialization

We can specialize a class template for a category of types.

cpp

template <typename T>

class Printer

{

};


For pointers:

cpp

template <typename T>

class Printer<T*>

{

};


Now:

cpp

Printer<int> p1;     // primary template

Printer<int*> p2;    // pointer specialization


Important interview question

Can function templates be partially specialized?

No.

Class templates can be partially specialized.

Function templates can instead be overloaded.


───


4. What's the difference between function template specialization and overloading?

Consider:

cpp

template <typename T>

void print(T value)

{

    std::cout << "Template\n";

}


We can specialize it:

cpp

template <>

void print<int>(int value)

{

    std::cout << "Specialization\n";

}


Or we can overload it:

cpp

void print(int value)

{

    std::cout << "Overload\n";

}


These are not equivalent.

Why?

Overloading participates in overload resolution.

Template specialization happens after the appropriate template has been selected.

This can lead to surprising behavior, particularly when templates are called through other templates.

Interview recommendation

If asked:

"Should I specialize a function template or overload it?"

A good answer is:

Function templates generally cannot be partially specialized, and overloading is often preferable when customizing function behavior. Class template specialization is the normal mechanism for specialization.


───


5. What is SFINAE?

SFINAE stands for:

Substitution Failure Is Not An Error

It means that when substituting template arguments causes an invalid type or expression in the immediate context, that template can simply be removed from consideration rather than producing a compilation error.

A classic example:

cpp

template <typename T>

typename T::value_type getValue(T obj)

{

    return obj[0];

}


Suppose:

cpp

std::vector<int> v;

getValue(v);


std::vector<int> has:

cpp

value_type


so the template works.

But:

cpp

int x;

getValue(x);


doesn't have int::value_type.

With SFINAE, we can use this fact to control which functions participate in overload resolution.

Common SFINAE tools

cpp

std::enable_if

std::void_t

std::is_same

std::is_integral

std::is_convertible


For example:

cpp

template <

    typename T,

    typename = std::enable_if_t<std::is_integral_v<T>>

>

void foo(T value)

{

    std::cout << "Integral\n";

}


This function only participates when T is an integral type.

Modern C++

C++20 concepts provide a much cleaner solution:

cpp

template <std::integral T>

void foo(T value)

{

}


So an excellent interview answer is:

SFINAE was historically used to constrain templates. In modern C++, concepts are usually preferred because they're clearer and produce better diagnostics.


───


6. What are variadic templates?

A variadic template can accept zero or more template arguments.

cpp

template <typename... Args>

void print(Args... args)

{

}


Args... is called a parameter pack.

For example:

cpp

print(1, 2.5, "hello", 'A');


Args could represent:

text

int

double

const char*

char


Fold expressions

C++17 introduced fold expressions, making variadic templates much easier to use.

For example:

cpp

template <typename... Args>

auto sum(Args... args)

{

    return (args + ...);

}


Then:

cpp

auto result = sum(1, 2, 3, 4);


Conceptually:

cpp

((1 + 2) + 3) + 4


Why are variadic templates useful?

They're heavily used in:

* std::tuple

* std::make_unique

* std::make_shared

* std::format

* Generic wrappers

* Perfect forwarding


───


7. What is template instantiation?

A template is essentially a blueprint. Instantiation happens when the compiler needs a concrete version of that template.

Example:

cpp

template <typename T>

T square(T x)

{

    return x * x;

}


When we write:

cpp

square(5);


the compiler can instantiate:

cpp

int square(int x)

{

    return x * x;

}


And:

cpp

square(2.5);


can result in:

cpp

double square(double x)

{

    return x * x;

}


Three concepts worth knowing

Implicit instantiation

cpp

square(5);


The compiler generates what's needed automatically.

Explicit specialization

cpp

template <>

int square<int>(int x)

{

    // special implementation

}


Explicit instantiation

cpp

template int square<int>(int);


This explicitly tells the compiler to instantiate the template for int.

Interview follow-up

Why are templates usually defined in header files?

Because the compiler generally needs to see the template definition at the point of instantiation.

This is why you'll commonly see:

cpp

// MyClass.h

template <typename T>

class MyClass

{

    ...

};


rather than putting the implementation only in a .cpp file.


───


8. What are dependent names, and why do we need typename?

This is one of the classic advanced template questions.

Consider:

cpp

template <typename T>

void foo()

{

    T::value_type x;

}


The problem is that T is a dependent type. The compiler doesn't know what T will be yet.

Therefore, it doesn't know whether:

cpp

T::value_type


is a type or something else.

We tell the compiler:

cpp

template <typename T>

void foo()

{

    typename T::value_type x;

}


The typename says:

Treat T::value_type as a type.

Another related keyword: template

You can encounter:

cpp

obj.template foo<int>();


The template keyword tells the compiler that foo should be interpreted as a template when obj depends on a template parameter.

These two keywords are frequently tested together in senior C++ interviews.


───


9. What are C++20 concepts?

Concepts allow us to specify requirements on template parameters.

Before C++20, you might write:

cpp

template <

    typename T,

    typename = std::enable_if_t<std::is_arithmetic_v<T>>

>

T add(T a, T b)

{

    return a + b;

}


This works, but it's difficult to read.

With C++20:

cpp

template <typename T>

concept Numeric = std::is_arithmetic_v<T>;


template <Numeric T>

T add(T a, T b)

{

    return a + b;

}


Or using a standard concept:

cpp

template <std::integral T>

T add(T a, T b)

{

    return a + b;

}


Advantages of concepts

* More readable

* Better compiler errors

* Clearly communicates requirements

* Cleaner overload resolution

* Replaces many traditional SFINAE techniques

Interview answer

A good concise answer:

Concepts are named compile-time constraints on template parameters introduced in C++20. They make template requirements explicit and generally provide clearer diagnostics than SFINAE.


───


10. What is perfect forwarding, and how is it related to templates?

This is probably the most important advanced topic on this list.

Consider:

cpp

template <typename T>

void wrapper(T&& arg)

{

    foo(std::forward<T>(arg));

}


Here T&& can be a forwarding reference when T is deduced.

It allows the wrapper to preserve whether the caller passed an lvalue or rvalue.

Example

cpp

void foo(const std::string& s)

{

    std::cout << "lvalue\n";

}


void foo(std::string&& s)

{

    std::cout << "rvalue\n";

}


Now:

cpp

std::string s = "hello";


wrapper(s);                  // lvalue

wrapper(std::string("hi"));  // rvalue


std::forward<T> preserves that value category.

Why not just use std::move?

This is an important interview question.

std::moveunconditionally casts its argument to an rvalue.

std::forward<T> conditionally casts based on the original type/value category.

So:

cpp

std::move(x)


means roughly:

"Treat x as an rvalue."

Whereas:

cpp

std::forward<T>(x)


means:

"Preserve whether the caller originally gave me an lvalue or rvalue."

Reference collapsing

Perfect forwarding relies on reference collapsing rules:


Combination

Result


T& &

T&


T& &&

T&


T&& &

T&


T&& &&

T&&



This is very commonly asked in senior C++ interviews.


───


🔥 The 10 to memorize

If you want a compact interview revision list:


#

Question

Key concept


1

What are templates?

Generic/compile-time programming


2

typename vs class?

Template parameters + dependent types


3

What is specialization?

Full vs partial specialization


4

Specialization vs overloading?

Overload resolution


5

What is SFINAE?

Substitution failure


6

What are variadic templates?

Parameter packs/fold expressions


7

What is template instantiation?

Implicit/explicit instantiation


8

What are dependent names?

typename, template


9

What are concepts?

C++20 constraints


10

What is perfect forwarding?

Forwarding references/reference collapsing



For a senior C++ interview, I'd spend the most time on #5, #8, #9 and #10. Those are where interviewers can quickly move from basic template knowledge into deeper C++ understanding.

C++ lambda syntax cheat sheet from basics → advanced

 Here’s a C++ lambda syntax cheat sheet from basics → advanced, with the pieces building up progressively.

1. Basic lambda
cpp
[]() {
    std::cout << "Hello";
};
General syntax:
cpp
[capture](parameters) -> return_type {
    // body
};
The -> return_type is usually optional because C++ can deduce it.
cpp
[]() {
    return 10;
};

───
2. Store a lambda in a variable
cpp
auto add = [](int a, int b) {
    return a + b;
};
std::cout << add(2, 3);   // 5
Think of a lambda as an unnamed function object.

───
3. Parameters
cpp
auto multiply = [](int a, int b) {
    return a * b;
};
multiply(3, 4);
Explicit return type:
cpp
auto divide = [](int a, int b) -> double {
    return static_cast<double>(a) / b;
};

───
4. Capture list []
The capture list controls which outside variables the lambda can access.
cpp
int x = 10;
auto f = []() {
    // std::cout << x;  // ❌ x not captured
};
Capture x by value:
cpp
int x = 10;
auto f = [x]() {
    std::cout << x;
};
Capture x by reference:
cpp
int x = 10;
auto f = [&x]() {
    x = 20;
};

───
5. Capture everything
Capture everything by value
cpp
int x = 10;
int y = 20;
auto f = [=]() {
    std::cout << x << y;
};
Capture everything by reference
cpp
int x = 10;
int y = 20;
auto f = [&]() {
    x++;
    y++;
};
Mix value and reference
cpp
int x = 10;
int y = 20;
auto f = [x, &y]() {
    // x → copied
    // y → referenced
};
You can also write:
cpp
[x, &y]

───
6. Mutable lambda
This is an important concept.
By default, variables captured by value cannot be modified inside the lambda:
cpp
int x = 10;
auto f = [x]() {
    // x++;  // ❌
};
Use mutable:
cpp
int x = 10;
auto f = [x]() mutable {
    x++;
    std::cout << x;
};
f();  // 11
f();  // 12
std::cout << x;  // 10
Notice:
text
Original x       = 10
Lambda's copy    = 10 → 11 → 12
mutable changes the lambda's captured copy, not the original variable.

───
7. Lambda with no parameters
cpp
auto hello = [] {
    std::cout << "Hello";
};
The () can be omitted when there are no parameters.
Equivalent:
cpp
[]() {
    std::cout << "Hello";
};

───
8. Lambda with return type
Usually:
cpp
auto f = [](int x) {
    return x * 2;
};
Compiler deduces int.
Explicit:
cpp
auto f = [](int x) -> int {
    return x * 2;
};
Useful when deduction is problematic or when you want to be explicit.

───
9. Lambda passed to an algorithm
This is where lambdas become extremely useful.
cpp
std::vector<int> v = {1, 2, 3, 4, 5};
std::for_each(v.begin(), v.end(), [](int x) {
    std::cout << x << " ";
});
Sorting:
cpp
std::sort(v.begin(), v.end(), [](int a, int b) {
    return a > b;
});
Result:
text
5 4 3 2 1

───
10. Lambda with std::find_if
cpp
auto it = std::find_if(
    v.begin(),
    v.end(),
    [](int x) {
        return x > 10;
    }
);
The lambda acts as a predicate.

───
11. Generic lambda — C++14
Instead of specifying parameter types:
cpp
auto print = [](auto x) {
    std::cout << x;
};
print(10);
print(3.14);
print("Hello");
The lambda effectively behaves like a function template.
Conceptually:
text
print(int)
print(double)
print(const char*)

───
12. Generic lambda with multiple parameters
cpp
auto add = [](auto a, auto b) {
    return a + b;
};
add(10, 20);
add(2.5, 3.5);

───
13. Generic lambda with forwarding references
More advanced:
cpp
auto f = [](auto&& x) {
    // x can bind to lvalue or rvalue
};
This is commonly used when writing generic code.
For perfect forwarding:
cpp
auto f = [](auto&& x) {
    some_function(std::forward<decltype(x)>(x));
};

───
14. Lambda returning a lambda
Yes, lambdas can return lambdas.
cpp
auto createAdder = [](int x) {
    return [x](int y) {
        return x + y;
    };
};
auto add10 = createAdder(10);
std::cout << add10(5);  // 15
Here:
text
createAdder(10)
       ↓
lambda capturing x = 10
       ↓
add10(5)
       ↓
15

───
15. Immediately Invoked Lambda — IIFE
You can create and immediately execute a lambda:
cpp
int result = [](int a, int b) {
    return a + b;
}(10, 20);
result becomes 30.
This:
cpp
[](int a, int b) {
    return a + b;
}(10, 20);
means:
Create lambda → immediately call it with 10, 20.


───
16. Capture this
Inside a member function:
cpp
class A {
    int value = 10;
public:
    void foo() {
        auto f = [this]() {
            std::cout << value;
        };
        f();
    }
};
[this] captures the this pointer.
You can therefore access:
cpp
value
which is essentially:
cpp
this->value

───
17. Capture *this — C++17
This is different.
cpp
auto f = [*this]() {
    std::cout << value;
};
[*this] captures a copy of the object.
Compare:
cpp
[this]
with:
cpp
[*this]

Capture
Captures
[this]
this pointer
[*this]
copy of the object

This distinction becomes important when the lambda outlives the object.


───
18. Init capture — C++14
You can create a new variable inside the capture list:
cpp
int x = 10;
auto f = [y = x + 5]() {
    std::cout << y;
};
y is a new variable belonging to the lambda.
Very useful for moving objects:
cpp
auto ptr = std::make_unique<int>(10);
auto f = [p = std::move(ptr)]() {
    std::cout << *p;
};
Now ownership of the unique_ptr has been moved into the lambda.


───
19. Generalized lambda capture
You can have multiple init captures:
cpp
int x = 10;
auto f = [
    a = x + 1,
    b = x * 2
]() {
    std::cout << a << b;
};

───
20. constexpr lambda
Modern C++ allows constexpr lambdas:
cpp
constexpr auto square = [](int x) {
    return x * x;
};
constexpr int result = square(5);
result can be evaluated at compile time.
───


21. Lambda with constraints — C++20
Generic lambda:
cpp
auto add = [](auto a, auto b) {
    return a + b;
};
You can constrain it:
cpp
auto add = []<typename T>(T a, T b) {
    return a + b;
};
With concepts:
cpp
auto add = []<std::integral T>(T a, T b) {
    return a + b;
};
Now the lambda only accepts integral types.
This <typename T> syntax is called a template parameter list for the lambda and was introduced in C++20.


───
22. Lambda conversion to function pointer
A lambda with no captures can convert to a function pointer:
cpp
auto f = [](int x) {
    return x * 2;
};
int (*ptr)(int) = f;
std::cout << ptr(5);
But this doesn't work for a capturing lambda:
cpp
int x = 10;
auto f = [x](int y) {
    return x + y;
};
// int (*ptr)(int) = f;  // ❌
Why?
Because the lambda needs stored state (x).


───
23. std::function
You can store lambdas in std::function:
cpp
std::function<int(int, int)> add =
    [](int a, int b) {
        return a + b;
    };
std::cout << add(2, 3);
This is useful when you need a common callable type.
But std::function has some overhead, so don't automatically use it everywhere.


───
24. Lambda as a comparator
Very common in interviews:
cpp
std::sort(v.begin(), v.end(),
    [](int a, int b) {
        return a < b;
    });
For objects:
cpp
std::sort(students.begin(), students.end(),
    [](const Student& a, const Student& b) {
        return a.age < b.age;
    });


───
25. Recursive lambda
A lambda cannot simply refer to itself by its own variable during its initialization:
cpp
// ❌
auto factorial = [](int n) {
    return n * factorial(n - 1);
};
One common solution is std::function:
cpp
std::function<int(int)> factorial =
    [&](int n) {
        if (n <= 1)
            return 1;
        return n * factorial(n - 1);
    };
Modern C++ also allows more efficient patterns using an explicit self parameter:
cpp
auto factorial = [](this auto&& self, int n) {
    if (n <= 1)
        return 1;
    return n * self(n - 1);
};
The explicit object parameter form above is C++23.


───
26. The full syntax to remember
The most useful mental model is:
cpp
[captures] <template_params> (parameters)
    mutable
    constexpr
    noexcept
    -> return_type
{
    body
}
Not every part is required.
For example:
cpp
auto f =
    [x, &y]                 // capture
    <typename T>            // template parameters (C++20)
    (T value)               // parameters
    mutable                 // mutable
    noexcept                // noexcept
    -> T                    // return type
{
    // body
};

───
The progression I'd memorize
text
[]() { }
 ↓
[](int x) { }
 ↓
[x](int y) { }
 ↓
[&x](int y) { }
 ↓
[=](int y) { }
 ↓
[&](int y) { }
 ↓
[x]() mutable { }
 ↓
[x = std::move(obj)]() { }
 ↓
[](auto x) { }                    // C++14
 ↓
[*this]() { }                     // C++17
 ↓
[]<typename T>(T x) { }           // C++20
 ↓
[](this auto&& self) { }          // C++23
If you're preparing for a C++ interview, the most important lambda topics are: capture by value/reference, mutable, this vs *this, init-capture, generic lambdas, lambda-to-function-pointer conversion, std::function, and using lambdas with STL algorithms.