C++ Cheatsheet

Concurrency

Use this C++ reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

std::thread and std::jthread

#include <thread>

void work(int id) { /* ... */ }

std::thread t(work, 42);          // starts immediately; args copied into the thread
t.join();                         // wait for completion (must join or detach!)
// or: t.detach();                // run independently (rarely what you want)
t.joinable();                     // true if not yet joined/detached

std::thread t2([]{ /* lambda body */ });

// Pass by reference: wrap in std::ref
void inc(int& x) { ++x; }
int n = 0;
std::thread t3(inc, std::ref(n));
t3.join();

// C++20 jthread: joins automatically in its destructor + stop tokens
std::jthread jt([](std::stop_token st) {
    while (!st.stop_requested()) { /* work */ }
});
jt.request_stop();                // cooperative cancellation

std::thread::hardware_concurrency();       // hint: # of hardware threads
std::this_thread::get_id();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::this_thread::yield();

A destructed std::thread that is still joinable calls std::terminate — prefer std::jthread (C++20).

Mutexes and Locks

#include <mutex>
#include <shared_mutex>

std::mutex m;

// Prefer RAII guards over manual lock()/unlock()
{
    std::lock_guard<std::mutex> lk(m);      // locks now, unlocks at scope exit
    // critical section
}

{
    std::unique_lock<std::mutex> lk(m);     // movable; supports manual unlock
    lk.unlock();
    lk.lock();
}                                            // needed for condition_variable

// C++17 scoped_lock — locks multiple mutexes deadlock-free
std::mutex m1, m2;
{
    std::scoped_lock lk(m1, m2);
}

// try_lock — non-blocking
if (m.try_lock()) { /* got it */ m.unlock(); }

// Recursive (same thread may relock) and timed variants
std::recursive_mutex rm;
std::timed_mutex tm;
tm.try_lock_for(std::chrono::milliseconds(10));

// Reader/writer lock (C++17)
std::shared_mutex sm;
{ std::shared_lock lk(sm); /* many concurrent readers */ }
{ std::unique_lock lk(sm); /* single writer */ }

// Run initialization exactly once across threads
std::once_flag flag;
std::call_once(flag, []{ /* init */ });

Condition Variables

#include <condition_variable>
#include <queue>

std::mutex m;
std::condition_variable cv;
std::queue<int> q;
bool done = false;

// Producer
{
    std::lock_guard<std::mutex> lk(m);
    q.push(1);
}
cv.notify_one();      // or notify_all()

// Consumer — the predicate overload handles spurious wakeups
{
    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, []{ return !q.empty() || done; });   // atomically unlocks + sleeps
    if (!q.empty()) { int v = q.front(); q.pop(); }
}

// Timed wait
cv.wait_for(lk, std::chrono::seconds(1), []{ return done; });  // false on timeout

Always wait with a predicate and always modify the shared state under the mutex before notifying.

std::atomic

#include <atomic>

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

counter++;  counter--;            // atomic read-modify-write
counter += 5;
counter.load();                   // read
counter.store(10);                // write
counter.exchange(7);              // set, return old value
counter.fetch_add(1);             // add, return old value

// Compare-and-swap (basis of lock-free structures)
int expected = 7;
counter.compare_exchange_strong(expected, 42);  // if == expected → 42; else expected = current

std::atomic<bool> ready{false};
std::atomic<Node*> head{nullptr};       // atomic pointers work too

std::atomic<int>::is_always_lock_free;  // constexpr: true on mainstream platforms

// Default ordering is memory_order_seq_cst (safest). Relaxed is for pure counters:
counter.fetch_add(1, std::memory_order_relaxed);

// C++20: atomic wait/notify (futex-like)
ready.wait(false);        // block until value != false
ready.store(true);
ready.notify_all();

std::atomic protects a single variable; invariants across multiple variables still need a mutex. volatile is NOT a threading tool.

Futures, Promises, and std::async

#include <future>

// std::async — simplest way to run a task and get a result
std::future<int> f = std::async(std::launch::async, []{ return 42; });
int result = f.get();     // blocks until ready; get() only once per future

// Launch policies: std::launch::async (new thread) | std::launch::deferred (lazy, on get())

// Exceptions propagate through the future
auto f2 = std::async(std::launch::async, []{ throw std::runtime_error("boom"); return 1; });
try { f2.get(); } catch (const std::exception& e) { /* "boom" */ }

// promise/future — hand-rolled one-shot channel
std::promise<int> p;
std::future<int> fut = p.get_future();            // get_future() only once per promise
std::thread producer([&p]{ p.set_value(7); });   // or p.set_exception(...)

// Status polling (before get(); the future must still be valid)
fut.wait();                                       // block until ready
fut.wait_for(std::chrono::milliseconds(5));       // future_status::ready / timeout / deferred

fut.get();                                        // 7 — invalidates the future
producer.join();

// shared_future — multiple consumers may get()
std::promise<int> p2;
std::shared_future<int> sf = p2.get_future().share();

// packaged_task — wrap a callable, run it wherever, future gets the result
std::packaged_task<int(int)> task([](int x){ return x * 2; });
std::future<int> fr = task.get_future();
std::thread(std::move(task), 21).detach();
fr.get();   // 42

A std::async future's destructor blocks until the task finishes — don't discard it if you want fire-and-forget.

Thread-Local Storage and Parallel Algorithms

// One instance per thread
thread_local int perThreadCounter = 0;

// Parallel STL algorithms (C++17, <execution>; link TBB on GCC/Clang)
#include <execution>
#include <algorithm>

std::sort(std::execution::par, v.begin(), v.end());          // parallel
std::sort(std::execution::par_unseq, v.begin(), v.end());    // parallel + vectorized
std::for_each(std::execution::par, v.begin(), v.end(),
              [](int& x){ x *= 2; });                         // body must be thread-safe
std::reduce(std::execution::par, v.begin(), v.end(), 0);     // parallel sum

Data race = UB. Two threads touching the same object where at least one writes, without synchronization (mutex/atomic), is undefined behavior — build with -fsanitize=thread to catch races in tests.