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::threadthat is still joinable callsstd::terminate— preferstd::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::atomicprotects a single variable; invariants across multiple variables still need a mutex.volatileis 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::asyncfuture'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.