The deep end. Templates, the full STL, lambdas, smart pointers, move semantics, exception handling, file I/O, and every modern C++11/14/17/20 feature you need to write professional, production-grade C++. This file is where you stop writing code and start engineering software.
A template is a blueprint the compiler uses to generate code for specific types. Write the algorithm once — the compiler stamps out correct versions for every type you use it with. This happens entirely at compile time — zero runtime overhead versus hand-written per-type functions.
When you write maxVal(3, 5), the compiler looks at the template, deduces T = int, and generates a concrete int maxVal(int, int) function just for integers. This is instantiation. If you later call it with double, it generates a totally separate function for double. Because the compiler must see the full template source code to generate these new functions on demand, templates must be entirely written in header files (.h/.hpp)—you cannot hide their implementation in a .cpp file! Furthermore, if you instantiate a class like std::vector with 50 different types, the compiler literally generates 50 full copies of the vector code. This is called Template Code Bloat and explains why heavy template usage balloons executable sizes and compile times.
Class templates let you build generic data structures — the foundation of the entire STL. vector<T>, pair<K,V>, map<K,V> — all class templates.
Variadic templates accept any number of type parameters. Combined with C++17 fold expressions, they unlock elegant recursive and pack-processing code.
Before C++20, template errors produced famously unreadable error messages. Concepts let you express constraints on template parameters explicitly — better errors, self-documenting code.
Without concepts, use static_assert with type traits: static_assert(std::is_integral_v<T>, "T must be integral");. It's less elegant but works back to C++11 and gives a reasonable error message.
| Trap | Problem | Fix |
|---|---|---|
| Template in .cpp file | Linker error — templates must be fully defined where used | Put template definitions in header files (.h/.hpp), not .cpp |
| Implicit narrowing in deduction | maxVal(3, 3.5) — T deduced as int AND double → ambiguity error | Be explicit: maxVal<double>(3, 3.5) or cast: maxVal(3.0, 3.5) |
| Code bloat | Templates generate separate code for every instantiated type | Use extern template to suppress redundant instantiations in multi-TU projects |
| Unreadable errors (pre-C++20) | 40-line error message for a simple type mismatch | Add static_assert with clear messages; upgrade to C++20 concepts |
| Container | Type | Access | Insert/Delete | Use When |
|---|---|---|---|---|
vector<T> | Sequence | O(1) random | O(1) end / O(n) mid | Default choice. Contiguous memory, cache-friendly. |
deque<T> | Sequence | O(1) random | O(1) front & back | Need fast push/pop at both ends. |
list<T> | Sequence | O(n) linear | O(1) anywhere | Frequent mid-sequence insert/delete with existing iterator. |
array<T,N> | Sequence | O(1) random | Fixed size | Fixed-size stack array with STL interface. |
map<K,V> | Associative | O(log n) | O(log n) | Sorted key-value pairs. Ordered iteration. |
unordered_map<K,V> | Hash | O(1) avg | O(1) avg | Fast lookup by key. Order doesn't matter. |
set<T> | Associative | O(log n) | O(log n) | Unique sorted elements. Membership testing. |
unordered_set<T> | Hash | O(1) avg | O(1) avg | Fast unique membership. Order doesn't matter. |
stack<T> | Adaptor | top only | O(1) top | LIFO. Wraps deque by default. |
queue<T> | Adaptor | front/back | O(1) | FIFO. BFS, task queues. |
priority_queue<T> | Adaptor | top only | O(log n) | Max-heap by default. Dijkstra, scheduling. |
push_back(obj) copies or moves an existing object into the vector. emplace_back(args...) constructs the object directly inside the vector's storage — zero copy, zero move. For complex objects, always prefer emplace_back. For simple types like int, the difference is negligible.
When push_back causes a reallocation (capacity exceeded), all iterators, pointers, and references to the vector are invalidated. Never hold a pointer into a vector while modifying it. If you need stable references, use reserve(n) upfront, or use a deque or list.
myMap["key"] is NOT a read-only lookup. If "key" doesn't exist, it inserts a default-constructed value for that key. This is usually a bug in read-only contexts. Use myMap.find("key") or myMap.at("key") when you want to check/read without inserting. This is one of the most common map bugs.
| Trap | Wrong | Correct |
|---|---|---|
| map[] inserts on read | if (m["key"] == 0) — inserts "key" with 0! | if (m.count("key") && m["key"] == 0) or m.find() |
| Iterating while erasing | for(auto it=v.begin(); it!=v.end(); it++) if(bad) v.erase(it); | it = v.erase(it); — erase returns next valid iterator |
| vector reallocation | Storing pointer to v[0] then push_back — dangling pointer | reserve(n) upfront or use indices, not pointers |
| set::erase all duplicates | s.erase(val) on multiset removes ALL copies | s.erase(s.find(val)) removes exactly one copy |
| Comparing size() to -1 | if (v.size() - 1 >= 0) — size() is unsigned! Always true | Cast: if ((int)v.size() - 1 >= 0) or check v.empty() first |
An iterator is an object that points into a container and can advance through it. The STL's power comes from algorithm + iterator + container independence — any algorithm works with any container that provides compatible iterators.
| Trap | Wrong | Correct |
|---|---|---|
| unique doesn't shrink vector | unique(v.begin(), v.end()); — "removed" elements still in memory | auto e = unique(…); v.erase(e, v.end()); to actually remove |
| binary_search on unsorted range | binary_search on unsorted — undefined behavior | sort first, then binary_search/lower_bound/upper_bound |
| lower_bound return value | int idx = lower_bound(…, val); — returns iterator, not int | int idx = lower_bound(v.begin(), v.end(), val) - v.begin(); |
| Modifying range in for_each | for_each(…, [](int x){x++;}) — x is a copy! | for_each(…, [](int& x){x++;}) — use reference |
RAII is the defining idiom that makes modern C++ safe. In languages like Java or C#, you rely on a Garbage Collector to clean up memory eventually, but you still have to manually close file handles and release locks (usually in scattered finally blocks). In C++, you tie all resources (heap memory, network sockets, DB connections, mutexes) to the lifespan of an object on the stack. When the object is created (Acquisition), it opens the file/memory. When the object goes out of scope—for any reason, including early return or thrown exceptions—its destructor runs instantly and deterministically cleans up the resource. There is no Garbage Collector needed, no finally blocks to remember, and no leaks if you follow this rule. Smart pointers are simply RAII applied to heap memory.
A unique_ptr owns an object exclusively — one pointer, one owner. When the unique_ptr goes out of scope, the object is automatically deleted. Zero overhead compared to raw pointers. Use this for 90% of heap allocations.
A shared_ptr uses reference counting — multiple pointers can own the same object. The object is deleted when the last shared_ptr to it is destroyed. Has overhead: an atomic reference count per object.
If object A holds a shared_ptr to B, and B holds a shared_ptr back to A, neither will ever be destroyed — ref count never reaches zero. Break cycles with weak_ptr: use shared_ptr for the "owning" direction and weak_ptr for the "back" reference.
Unique ownership → unique_ptr (default for 90% of cases).
Shared ownership (truly multiple owners) → shared_ptr.
Observe without owning (cache, back-pointer, cycle break) → weak_ptr.
Never use raw new/delete in new code. When interfacing with C APIs, get the raw pointer with .get() — never release ownership.
| Trap | Wrong | Correct |
|---|---|---|
| Two shared_ptrs from raw pointer | shared_ptr<T> a(raw); shared_ptr<T> b(raw); — double free! | Use make_shared once; copy the shared_ptr to share ownership |
| Not using make_unique/make_shared | shared_ptr<T> p(new T()); — two allocations, exception-unsafe | make_shared<T>() — one allocation, exception-safe |
| Storing this in shared_ptr | shared_ptr<Foo> sp(this); — independent count, double free | Inherit enable_shared_from_this, use shared_from_this() |
| weak_ptr without lock | *wp — weak_ptr has no operator* (can't dereference directly) | auto sp = wp.lock(); if (sp) { use *sp; } |
| Passing unique_ptr by value to constructor | Foo(unique_ptr<T> p) — caller must std::move the arg | Foo(unique_ptr<T>&& p) or document that caller must move |
| Trap | Problem | Fix |
|---|---|---|
| Dangling reference capture | [&x] after x is destroyed — dangling reference | Use [x] (copy) when lambda outlives the variable |
| [=] captures this implicitly | [=] in a member function captures this by pointer — if object dies, UB | Explicitly [this] or [*this] (C++17 — copy of object) |
| std::function overhead | Using std::function for every lambda — heap alloc, virtual dispatch | Use auto or template parameter for callbacks in performance-critical code |
| Recursive lambda | auto f = [](int n){ return n==0?1:n*f(n-1); } — f not captured yet! | Use std::function<int(int)> f = [&f](int n){...}; — captures f by ref |
Before exceptions, functions returned error codes (e.g., -1) to indicate failure. This creates huge problems: 1) You must litter every single function call with if (status == -1) return -1;, drowning the "happy path" in error-checking noise. 2) If you forget to check the return code, the program silently proceeds in a corrupted state. 3) If a function perfectly computes an int and the answer happens to be -1, you have overlapping data types. Exceptions solve this by completely separating error-handling from logic, and unlike return codes, an exception cannot be silently ignored. It demands attention or terminates the program safely.
When you throw an exception, C++ begins a process called Stack Unwinding. It works backwards through the call stack, doing two things at each level until it finds a matching catch block: it checks for a try block, and if it doesn't find one, it destroys all local variables in that scope (calling their destructors). This is why RAII + Exceptions makes C++ uniquely safe—the unwinding process perfectly vacuums up all memory, closes all files, and releases all locks as it flies back up the stack!
| Exception Class | Header | Use For |
|---|---|---|
std::exception | <exception> | Base class for all standard exceptions |
std::runtime_error | <stdexcept> | Errors detected only at runtime |
std::logic_error | <stdexcept> | Programming errors, invariant violations |
std::invalid_argument | <stdexcept> | Bad function argument |
std::out_of_range | <stdexcept> | Index / value out of valid range |
std::overflow_error | <stdexcept> | Arithmetic overflow |
std::bad_alloc | <new> | Memory allocation failure (new throws this) |
std::bad_cast | <typeinfo> | Failed dynamic_cast to reference |
No guarantee — the program may be in an inconsistent state if an exception occurs.
Basic guarantee — no resources are leaked; the program is in some valid (but unspecified) state.
Strong guarantee — the operation either succeeds completely, or has no effect (commit-or-rollback).
No-throw guarantee — the operation never throws (noexcept). Highest level. Always achieve at minimum the basic guarantee.
With RAII, you get the basic guarantee automatically — when an exception unwinds the stack, all destructors run, all resources are freed. To get the strong guarantee, use the "copy-and-swap" idiom: work on a copy, then swap atomically if everything succeeded. The swap itself should be noexcept.
| Trap | Problem | Fix |
|---|---|---|
| Catch by value | catch(std::exception e) — copies and slices derived type | catch(const std::exception& e) — always catch by const reference |
| Catching most-general first | catch(exception) before catch(runtime_error) — specific never reached | Catch most-specific first, most-general last |
| Swallowing exceptions silently | catch(...) {} — hides bugs, impossible to debug | At minimum log; re-throw with throw; if you can't handle it |
| Throwing in destructor | ~Foo() throws — if already unwinding, std::terminate called | Never throw from destructors; catch internally and log/ignore |
| Using exceptions for control flow | throw/catch to break out of loops — extremely slow | Exceptions are for exceptional (error) conditions, not normal flow |
| Trap | Problem | Fix |
|---|---|---|
| Not checking if file opened | Read from closed ifstream — silently reads nothing | if (!inFile) { /* handle error */ } after opening |
| Mixing >> and getline | cin >> n; getline(cin, line); — reads empty line | cin.ignore(numeric_limits<streamsize>::max(), '\n') before getline |
| Not resetting stream state | After EOF or error, stream is in fail state — reads silently fail | inFile.clear(); before re-reading; check .good()/.fail() |
| setw is not sticky | cout << setw(10) — only affects the NEXT single output | fixed, scientific, setfill are sticky. setw must be repeated each time. |
| Feature | Trap | Correct Use |
|---|---|---|
| auto | auto s = "hello"; — deduced as const char*, NOT std::string | string s = "hello"; or auto s = string{"hello"}; |
| Structured bindings | auto [a,b] = pair; — a,b are copies of the pair members | auto& [a,b] = pair; — references to avoid copies |
| string_view lifetime | string_view sv = myFunc(); — if function returns temp string, sv is dangling | string_view only valid as long as the underlying string lives |
| optional dereference without check | *opt when opt is empty — undefined behavior | if (opt) or opt.has_value() before dereferencing |
| variant bad_variant_access | get<int>(v) when v holds double — throws bad_variant_access | Use holds_alternative<T>(v) first, or std::visit |
Deadlock occurs when thread A holds mutex 1 and waits for mutex 2, while thread B holds mutex 2 and waits for mutex 1. Prevention rules: always lock multiple mutexes in the same order, or use std::lock(m1, m2) which locks both atomically. Never hold a lock while calling user code that might try to acquire the same lock.
| Trap | Problem | Fix |
|---|---|---|
| Data race | Two threads read/write shared var without mutex — UB | Protect with mutex or use atomic<T> |
| Forgetting to join/detach | std::thread destructor called on joinable thread → std::terminate | Always call join() or detach() before thread goes out of scope |
| Deadlock | Lock same mutex twice in one thread / circular lock order | Lock in consistent order; use std::lock() for multiple; avoid nested locks |
| Spurious wakeups | condition_variable::wait wakes up without being notified | Always use predicate overload: cv.wait(lock, []{ return ready; }) |
| Race on shared_ptr refcount | Sharing shared_ptr between threads without protection | shared_ptr refcount is atomic but object access still needs mutex |
| Header | Contents |
|---|---|
<vector> | vector<T> |
<array> | array<T,N> — fixed-size stack array with STL interface |
<deque> | deque<T> — double-ended queue |
<list> | list<T> — doubly-linked list |
<forward_list> | forward_list<T> — singly-linked list |
<map> | map<K,V>, multimap<K,V> |
<unordered_map> | unordered_map<K,V>, unordered_multimap<K,V> |
<set> | set<T>, multiset<T> |
<unordered_set> | unordered_set<T>, unordered_multiset<T> |
<stack> | stack<T> |
<queue> | queue<T>, priority_queue<T> |
<algorithm> | sort, find, count, transform, reverse, unique, lower_bound… |
<numeric> | accumulate, iota, partial_sum, inner_product, reduce |
<memory> | unique_ptr, shared_ptr, weak_ptr, make_unique, make_shared |
<functional> | function<>, bind, placeholders, hash, less, greater |
<string> | string, wstring, to_string, stoi, stod |
<string_view> | string_view, wstring_view (C++17) |
<optional> | optional<T>, nullopt (C++17) |
<variant> | variant<Ts...>, get, holds_alternative, visit (C++17) |
<any> | any, any_cast (C++17) — type-erased single value |
<span> | span<T> — non-owning view (C++20) |
<ranges> | ranges algorithms, views (C++20) |
<thread> | thread, this_thread::sleep_for, hardware_concurrency |
<mutex> | mutex, lock_guard, unique_lock, scoped_lock |
<future> | future, promise, async, packaged_task |
<atomic> | atomic<T>, atomic_flag |
<chrono> | duration, time_point, steady_clock, system_clock |
<type_traits> | is_integral, is_pointer, remove_reference, enable_if… |
<concepts> | integral, floating_point, same_as, convertible_to… (C++20) |
Sole owner → unique_ptr
Shared owner → shared_ptr
Observer/cycle → weak_ptr
Always: make_unique/make_shared
Never: raw new/delete
[] — capture nothing
[=] — all by copy
[&] — all by reference
[x, &y] — x copy, y ref
[this] — this pointer
[*this] — copy of object (C++17)
vector random access: O(1)
vector push_back (amortized): O(1)
map/set insert/find: O(log n)
unordered_map find: O(1) avg
sort: O(n log n)
binary_search: O(log n)
C++11: auto, lambda, move, nullptr, enum class, unique_ptr
C++14: generic lambda, make_unique
C++17: structured binding, optional, variant, string_view, if-init
C++20: concepts, ranges, format, span, coroutines
mutex — exclusive lock
lock_guard — RAII lock (no unlock)
unique_lock — RAII + manual unlock
shared_mutex — reader-writer
atomic<T> — lock-free simple types
condition_variable — wait/notify
None — corruption possible
Basic — no leak, valid state
Strong — commit or rollback
No-throw — never throws
RAII gives Basic for free.
copy-and-swap gives Strong.
| # | Bug | Symptom | Fix |
|---|---|---|---|
| 1 | Iterator invalidation | push_back after storing iterator → crash | reserve() upfront or use indices |
| 2 | map[] on read | Silent insertion of default value | Use find() or count() for read-only access |
| 3 | Dangling lambda capture | [&x] after x destroyed → UB | [x] copy capture, or ensure lambda lifetime < captured var |
| 4 | Data race | Intermittent wrong results, crashes | mutex / lock_guard around all shared mutable state |
| 5 | Cyclic shared_ptr | Memory never freed, growing leak | Break cycle with weak_ptr |
| 6 | unique on unsorted | Only removes consecutive duplicates | sort → unique → erase |
| 7 | Throwing in destructor | std::terminate if second exception active | Catch all exceptions in destructor; never propagate |
| 8 | Missing noexcept on move | vector uses copy instead of move on resize | Mark move ctor/assign noexcept |
| 9 | string_view dangling | string_view into destroyed temporary string | string_view only for function params / temporaries in same expression |
| 10 | Thread not joined/detached | std::terminate on thread destructor | Always join() or detach() before thread goes out of scope |