Getting Started
Hello World
Every C++ program begins in main(). <iostream> provides std::cout (standard output) and std::cin (standard input). The std:: prefix refers to the standard namespace; using namespace std; can avoid it but is discouraged in headers because it pollutes the global namespace.
#include <iostream>
int main() {
std::cout << "Hello, World!" << std::endl;
return 0;
}Variables & auto
auto deduces the type from the initializer (C++11). Use auto when the type is obvious or verbose (iterators). const makes a value immutable; constexpr evaluates at compile time for true constants embedded in the binary.
int age = 30;
double pi = 3.14159;
char grade = 'A';
bool is_dev = true;
std::string name = "Alice";
auto x = 42; // int
auto y = 3.14; // double
const double TAX = 0.08;Input & Output
std::getline reads a full line including spaces, while std::cin >> stops at whitespace. Mixing them leaves a newline in the buffer; call std::cin.ignore() between getline and >> to discard it.
#include <iostream>
#include <string>
int main() {
std::string name;
int age;
std::cout << "Name: ";
std::getline(std::cin, name);
std::cout << "Age: ";
std::cin >> age;
std::cout << "Hi " << name << ", " << age << "\n";
}References
References are aliases that must be initialized and cannot be reseated to another object. Pass-by-reference avoids copying and allows modifying the caller's variable. Use const T& for read-only parameters to avoid expensive copies.
int x = 10;
int &ref = x; // alias for x
ref = 20;
std::cout << x; // 20
void increment(int &n) { n++; }
int a = 5;
increment(a); // a is now 6Type Conversion
Prefer static_cast over C-style casts because it is explicit and checked at compile time, making intent clear. std::stoi, std::stod convert strings to numbers; std::to_string does the reverse. Watch for std::out_of_range on bad input.
double d = 3.99;
int i = (int)d; // C-style: 3
int j = static_cast<int>(d); // C++: 3
int n = 65;
char c = static_cast<char>(n); // 'A'
// string <-> number
int num = std::stoi("42");
std::string s = std::to_string(3.14);Strings
std::string Basics
std::string manages its own memory and grows as needed. Unlike C char arrays, you don't manually manage length. .find() returns std::string::npos (a huge value) when the substring is not found, so always compare against npos.
#include <string>
std::string s = "Hello";
std::cout << s.length(); // 5
std::cout << s[0]; // H
s += ", World!";
std::cout << s; // Hello, World!
std::cout << s.substr(0, 3); // Hel
std::cout << s.find("World"); // 7Comparison & Search
Comparison is lexicographic (dictionary order). .find() searches forward, .rfind() searches backward. Both return std::string::npos if not found, so always compare against npos rather than treating the result as a boolean.
std::string a = "apple", b = "banana";
if (a == b) { /* equal */ }
if (a < b) { /* apple comes before banana */ }
if (a.find("pp") != std::string::npos) {
std::cout << "found\n";
}
size_t pos = a.rfind("p"); // last occurrenceStringstream
stringstream bridges strings and typed values, useful for building formatted strings (like a buffer) or parsing whitespace-separated tokens. It's slower than direct operations but very flexible for serialization and deserialization.
#include <sstream>
// Build a string
std::ostringstream oss;
oss << "Name=" << "Alice" << "&Age=" << 30;
std::string result = oss.str();
// Parse tokens
std::istringstream iss("10 20 30");
int a, b, c;
iss >> a >> b >> c; // a=10, b=20, c=30Raw Strings & Multiline
Raw string literals R"(...)" treat backslashes and quotes literally, ideal for regex patterns, Windows file paths, and JSON/XML templates. The delimiters inside the parens are arbitrary, e.g. R"x(...)x" to allow ) inside.
std::string raw = R"(C:\Users\name\file.txt)";
// No need to escape backslashes
std::string json = R"({
"name": "Alice",
"age": 30
})";char Arrays vs std::string
C-style char arrays require manual size management and are error-prone (buffer overflows). Prefer std::string; use .c_str() when interfacing with C APIs that expect const char*. Note that c_str() is only valid while the string is alive and unmodified.
char cstr[] = "Hello"; // null-terminated, size 6
std::string cppstr = "Hello";
#include <cstring>
std::cout << strlen(cstr); // 5
std::cout << cppstr.length(); // 5
std::string from_c = cstr; // C -> C++
const char* to_c = cppstr.c_str(); // C++ -> CNumbers & Math
Integer & Floating Types
Use <cstdint> fixed-width types (int32_t, int64_t) when exact size matters across platforms. The ' digit separator (C++14) improves readability of large numbers. double is the default floating type and is preferred over float for precision.
#include <cstdint>
int32_t a = 100;
int64_t big = 9'000'000'000LL;
uint8_t byte = 255;
size_t sz = sizeof(a); // 4
float f = 1.5f; // 4 bytes
double d = 3.14159265; // 8 bytes (preferred)Numeric Limits
<limits> provides type traits for numeric properties. Use these instead of hardcoded INT_MAX macros. epsilon() gives the smallest difference distinguishable by floating point, useful for comparing doubles with a tolerance.
#include <limits>
std::cout << std::numeric_limits<int>::max(); // 2147483647
std::cout << std::numeric_limits<int>::min(); // -2147483648
std::cout << std::numeric_limits<double>::infinity();
std::cout << std::numeric_limits<double>::epsilon();Math Functions
<cmath> provides standard math functions. Integer overflow is undefined behavior in C++; use int64_t or check bounds. For financial code, remember floating point is imprecise—consider integer cents or a decimal library.
#include <cmath>
double x = 2.5;
std::pow(x, 3); // 15.625
std::sqrt(x); // 1.581
std::abs(-5); // 5
std::floor(3.7); // 3.0
std::ceil(3.2); // 4.0
std::round(3.5); // 4.0
std::fmod(10.5, 3); // 1.5Random Numbers
Modern C++ uses the <random> library instead of rand(). mt19937 is a high-quality PRNG. Distributions (uniform_int, uniform_real, normal) map raw bits to the desired range without the modulo bias that plagues rand() % N.
#include <random>
std::random_device rd;
std::mt19937 gen(rd()); // Mersenne Twister engine
std::uniform_int_distribution<int> dist(1, 100);
for (int i = 0; i < 5; i++) {
std::cout << dist(gen) << " ";
}Integer Overflow & Casts
Signed integer overflow is undefined behavior in C++ (the compiler may optimize assuming it never happens). Always cast to a wider type before multiplying, or check bounds. Unsigned overflow wraps modulo 2^n and is well-defined.
int a = 100'000;
int b = a * a; // overflow! undefined behavior
long long safe = (long long)a * a; // OK
// Check before multiplying
if (a > 0 && b > INT_MAX / a) {
// would overflow, handle it
}Control Flow
If / Else
C++17 introduces if with initializer: if (auto it = m.find(k); it != m.end()) { ... }. This scopes the variable to the if/else block, keeping the surrounding scope clean and avoiding accidental reuse.
int score = 85;
if (score >= 90) {
std::cout << "A\n";
} else if (score >= 80) {
std::cout << "B\n";
} else {
std::cout << "C\n";
}Switch
Always include break to prevent unintended fall-through. C++17 [[fallthrough]] attribute documents intentional fall-through to silence warnings. Switch works on integral and enum types, not strings or floats.
int day = 3;
switch (day) {
case 1: std::cout << "Mon"; break;
case 2: std::cout << "Tue"; break;
case 3: std::cout << "Wed"; break;
default: std::cout << "Other";
}For Loops
Range-based for iterates containers cleanly. Use const auto& to avoid copying elements (important for strings and large objects). To modify elements in place, use auto& (non-const reference).
// Classic for
for (int i = 0; i < 5; i++) {
std::cout << i << " ";
}
// Range-based for (C++11)
std::vector<int> v = {1, 2, 3};
for (int n : v) std::cout << n;
for (const auto &n : v) std::cout << n; // no copyWhile & Do-While
while checks the condition before executing; do-while executes the body at least once before checking. do-while is useful for input validation and menu loops where the body must run before the condition can be evaluated.
int n = 5;
while (n > 0) {
std::cout << n-- << " ";
}
int x;
do {
std::cin >> x;
} while (x < 0); // runs at least onceBreak, Continue & Nested Loops
break exits the nearest enclosing loop; continue skips to the next iteration. C++ has no labeled break like Java; use a flag variable, or extract the loop into a function and use return to break out of nested loops.
for (int i = 0; i < 10; i++) {
if (i == 3) continue; // skip 3
if (i == 7) break; // stop at 7
std::cout << i << " "; // 0 1 2 4 5 6
}
// C++ has no labeled break; use a flag
bool found = false;
for (int i = 0; i < n && !found; i++) {
for (int j = 0; j < m; j++) {
if (matrix[i][j] == target) { found = true; break; }
}
}Functions & Lambdas
Define & Multiple Returns
C++17 structured bindings (auto [a, b] = ...) unpack tuples, pairs, and structs cleanly. Before C++17, use std::tie or output parameters. Returning by value is cheap due to move semantics (RVO) which elides the copy.
#include <tuple>
std::tuple<int, int> divide(int a, int b) {
return {a / b, a % b};
}
auto [q, r] = divide(17, 5); // q=3, r=2 (C++17)Default & Inline
Default arguments let callers omit trailing parameters. inline is a hint to the compiler to expand the function inline; modern compilers decide inlining themselves based on optimization flags, so inline is mostly about ODR (one definition rule).
inline int power(int base, int exp = 2) {
int r = 1;
for (int i = 0; i < exp; i++) r *= base;
return r;
}
// power(3) == 9, power(2, 5) == 32Function Overloading
Overloading lets functions share a name but differ by parameter types. The compiler picks the best match via overload resolution. Ambiguous overloads cause compile errors; prefer templates when the body is identical across types.
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
std::string add(std::string a, std::string b) { return a + b; }
add(1, 2); // int version
add(1.5, 2.5); // double version
add("a", "b"); // string versionLambda Expressions
Lambdas create anonymous function objects inline. The [] captures variables: [=] by value, [&] by reference, [x] specific by value, [&x] specific by ref. They are essential for STL algorithms and callbacks. Beware dangling references when capturing by reference.
auto square = [](int x) { return x * x; };
std::cout << square(5); // 25
int factor = 3;
auto multiply = [factor](int x) { return x * factor; };
std::vector<int> v = {1, 2, 3};
std::for_each(v.begin(), v.end(), [](int n) {
std::cout << n << " ";
});Function Pointers & std::function
std::function (from <functional>) holds any callable: functions, lambdas, functors. It's more flexible than raw function pointers but has a small runtime overhead due to type erasure. Use it for callbacks and storing callables in containers.
#include <functional>
int add(int a, int b) { return a + b; }
int (*fp)(int, int) = add; // raw function pointer
std::function<int(int, int)> f = add; // flexible wrapper
f = [](int a, int b) { return a * b; };
std::cout << f(3, 4); // 12STL Containers
vector
vector is a dynamic array and the default container choice. push_back is amortized O(1). .at() does bounds checking (throws std::out_of_range), operator[] does not. Call reserve() upfront if you know the size to avoid reallocations.
#include <vector>
std::vector<int> v = {1, 2, 3};
v.push_back(4);
v.pop_back();
v.size(); // 3
v[0] = 0;
v.at(5); // throws std::out_of_range
for (int n : v) std::cout << n;map & unordered_map
map keeps keys sorted (O(log n) operations); unordered_map uses hashing (O(1) average). Use map when you need ordered iteration or range queries; unordered_map for pure lookup speed. Iterating unordered_map is unordered.
#include <map>
#include <unordered_map>
std::map<std::string, int> ordered; // sorted by key (red-black tree)
ordered["Alice"] = 30;
std::unordered_map<std::string, int> hashed; // hash table
hashed["Bob"] = 25;
for (auto &[k, v] : ordered) { // C++17 structured binding
std::cout << k << ":" << v;
}set & unordered_set
set stores unique sorted elements (O(log n)). unordered_set is the hash-based version (O(1) average). Use them for deduplication and membership testing. lower_bound/upper_bound enable range queries on ordered sets.
#include <set>
std::set<int> s = {3, 1, 4, 1, 5};
// s contains: 1, 3, 4, 5 (sorted, unique)
s.insert(2);
s.erase(1);
if (s.count(4)) std::cout << "found";
auto it = s.lower_bound(3); // first >= 3array & deque
array is a fixed-size stack-allocated array with STL interface (safer than C arrays, no decay to pointer). deque (double-ended queue) supports O(1) push/pop at both ends, unlike vector which is O(n) at the front.
#include <array>
#include <deque>
std::array<int, 3> arr = {1, 2, 3}; // fixed size, stack-allocated
arr.size(); // 3
std::deque<int> dq = {1, 2, 3};
dq.push_front(0);
dq.push_back(4);
// dq: 0, 1, 2, 3, 4tuple & pair
tuple holds heterogeneous values of any types. pair is a 2-element tuple. Structured bindings (C++17) decompose them into named variables. Common when iterating maps whose elements are pairs of (key, value).
#include <tuple>
std::tuple<int, std::string, double> t = {1, "Alice", 3.14};
auto [id, name, val] = t; // C++17 structured binding
std::pair<int, int> p = {1, 2};
std::cout << p.first << p.second;
auto [a, b] = std::make_pair(10, 20);Pointers & Memory
Raw Pointers
Pointers store memory addresses. & gets the address, * dereferences. Pointer arithmetic works on arrays. Raw pointers don't track ownership, leading to leaks and dangling pointers—prefer smart pointers for owned resources.
int x = 10;
int *ptr = &x; // address of x
std::cout << *ptr; // 10 (dereference)
*ptr = 20;
std::cout << x; // 20
int arr[] = {1, 2, 3};
int *p = arr;
std::cout << *(p + 1); // 2References vs Pointers
References are safer (never null, always valid) and have cleaner syntax. Use references for function parameters and return values. Use pointers when null is a meaningful state or when you need to reassign what is pointed to.
int x = 10;
int &ref = x; // must init, cannot reseat
int *ptr = &x; // can be null, can reassign
ref = 20; // x = 20
*ptr = 30; // x = 30
// References cannot be null, safer for parameters
void foo(const std::string &s); // preferred
void bar(std::string *s); // s might be nullunique_ptr
unique_ptr is sole ownership of a heap object. It cannot be copied, only moved. Automatically deletes when it goes out of scope (RAII). This is the default smart pointer for most use cases—zero overhead over a raw pointer.
#include <memory>
auto p = std::make_unique<int>(42);
std::cout << *p; // 42
// auto p2 = p; // ERROR: cannot copy
auto p2 = std::move(p); // transfer ownership
// p is now nullptrshared_ptr & weak_ptr
shared_ptr uses reference counting; the object is freed when the last shared_ptr is destroyed. weak_ptr observes without affecting the count, breaking reference cycles. Avoid cycles of shared_ptr (they leak because the count never reaches zero).
#include <memory>
auto a = std::make_shared<int>(42);
auto b = a; // both point to same object
std::cout << a.use_count(); // 2
std::weak_ptr<int> w = a; // observer, no ownership
if (auto locked = w.lock()) {
std::cout << *locked; // 42
}RAII & new/delete
RAII ties resource lifetime to object scope: constructors acquire, destructors release. This guarantees cleanup even when exceptions propagate. Prefer vectors and smart pointers over manual new/delete—they implement RAII for you.
// Manual new/delete (avoid in modern C++)
int *p = new int(42);
delete p;
// RAII: resource acquisition is initialization
class Buffer {
int *data;
public:
Buffer(size_t n) : data(new int[n]) {}
~Buffer() { delete[] data; } // auto cleanup
};
// Modern: use containers/smart pointers instead
std::vector<int> buf(100); // no manual delete neededClasses & OOP
Class & Constructor
The member initializer list (: name(...), age(...)) initializes members before the body runs, more efficient than assignment in the body. Mark getters const to allow calling on const objects. std::move avoids copying the string parameter.
class Person {
std::string name;
int age;
public:
Person(std::string n, int a) : name(std::move(n)), age(a) {}
std::string getName() const { return name; }
int getAge() const { return age; }
};
Person p("Alice", 30);
std::cout << p.getName();Access Modifiers & Encapsulation
private members are accessible only within the class; protected allows subclasses; public is open to all. Encapsulation hides implementation details, exposing a stable interface. Use private by default and expose only what's needed.
class Account {
private:
double balance;
protected:
std::string owner;
public:
Account(double b) : balance(b) {}
double getBalance() const { return balance; }
void deposit(double amt) { if (amt > 0) balance += amt; }
};Inheritance & Virtual
virtual enables runtime polymorphism—calling speak() through an Animal* dispatches to Dog's version. Always declare a virtual destructor in base classes so deleting through a base pointer calls the derived destructor. override catches typos.
class Animal {
public:
virtual void speak() { std::cout << "..."; }
virtual ~Animal() = default; // virtual destructor!
};
class Dog : public Animal {
public:
void speak() override { std::cout << "Woof"; }
};
Animal *a = new Dog();
a->speak(); // Woof (polymorphism)
delete a;Abstract Classes & Interfaces
A pure virtual function (= 0) makes the class abstract—you cannot instantiate it. Classes with only pure virtuals act like Java interfaces. Concrete subclasses must implement all pure virtuals or they remain abstract.
class Shape {
public:
virtual double area() const = 0; // pure virtual
virtual ~Shape() = default;
};
class Circle : public Shape {
double r;
public:
Circle(double r) : r(r) {}
double area() const override {
return 3.14159 * r * r;
}
};Operator Overloading
Operator overloading lets user types work with +, <<, ==, etc. Overload only when the meaning is intuitive (math types, iterators). The << operator is commonly overloaded via friend for stream output, enabling cout << myObject.
class Vec2 {
double x, y;
public:
Vec2(double x, double y) : x(x), y(y) {}
Vec2 operator+(const Vec2 &o) const {
return {x + o.x, y + o.y};
}
friend std::ostream &operator<<(std::ostream &os, const Vec2 &v) {
return os << "(" << v.x << "," << v.y << ")";
}
};
Vec2 a(1, 2), b(3, 4);
std::cout << a + b; // (4,6)Templates & Generics
Function Templates
Function templates generate type-specific versions at compile time. The compiler deduces T from arguments; you can also specify it explicitly. Templates are zero-cost abstractions—no runtime overhead, but they increase compile time and binary size.
template <typename T>
T max_val(T a, T b) {
return (a > b) ? a : b;
}
std::cout << max_val(3, 7); // int: 7
std::cout << max_val(3.14, 2.71); // double: 3.14
std::cout << max_val<std::string>("a", "b"); // explicitClass Templates
Class templates parameterize entire classes over types. The standard containers (vector, map) are all templates. Template code must be in headers (or use explicit instantiation) because the compiler needs the full definition to generate code.
template <typename T>
class Stack {
std::vector<T> data;
public:
void push(T v) { data.push_back(v); }
T pop() { T v = data.back(); data.pop_back(); return v; }
bool empty() const { return data.empty(); }
};
Stack<int> si;
si.push(1); si.push(2);
Stack<std::string> ss;
ss.push("hi");Template Specialization
Full specialization provides a custom implementation for a specific type. Partial specialization (only for class templates) customizes for a category of types (e.g., all pointer types). Useful for optimizing or special-casing behavior.
template <typename T>
T identity(T x) { return x; }
// Full specialization for bool
template <>
bool identity<bool>(bool x) {
std::cout << "bool!";
return x;
}
identity(42); // generic
identity(true); // specializedVariadic Templates
Variadic templates accept any number of arguments via parameter packs (...). They recurse to process each argument. C++17 fold expressions simplify this: (std::cout << ... << args). Used heavily in std::make_shared, std::tuple.
template <typename T>
void print(T v) { std::cout << v << "\n"; }
template <typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first << ", ";
print(rest...); // recurse
}
print(1, "hello", 3.14); // 1, hello, 3.14Concepts (C++20)
Concepts (C++20) constrain template parameters with readable requirements, replacing arcane enable_if/SFINAE. They produce much clearer error messages when constraints aren't met. Use standard concepts like std::integral, std::floating_point, std::convertible_to.
#include <concepts>
template <std::integral T>
T add(T a, T b) { return a + b; }
template <typename T>
requires std::floating_point<T>
T half(T x) { return x / 2; }
// Abbreviated form
auto square(std::integral auto x) { return x * x; }STL Algorithms
sort & find
STL algorithms operate on iterator ranges [begin, end). sort is O(n log n). find is linear; for sorted ranges use binary_search/lower_bound (O(log n)). Pass custom comparators (lambdas or std::greater) for custom ordering.
#include <algorithm>
std::vector<int> v = {3, 1, 4, 1, 5, 9};
std::sort(v.begin(), v.end()); // 1 1 3 4 5 9
std::sort(v.begin(), v.end(), std::greater<>()); // descending
auto it = std::find(v.begin(), v.end(), 4);
if (it != v.end()) std::cout << "found";transform & for_each
transform maps each element to a new value (like map in functional languages). for_each applies a function for side effects. C++20 ranges allow v | views::transform(...) for a cleaner pipeline style without begin/end iterators.
std::vector<int> v = {1, 2, 3, 4};
std::vector<int> squared(v.size());
std::transform(v.begin(), v.end(), squared.begin(),
[](int x) { return x * x; });
// squared: 1 4 9 16
std::for_each(v.begin(), v.end(), [](int &x) { x *= 2; });
// v: 2 4 6 8accumulate & count
accumulate (from <numeric>) folds a range with an operation. The third argument is the initial value and determines the result type—use 0.0 for double sums. count returns how many elements equal a value; count_if uses a predicate.
#include <numeric>
std::vector<int> v = {1, 2, 3, 4, 5};
int sum = std::accumulate(v.begin(), v.end(), 0); // 15
int product = std::accumulate(v.begin(), v.end(), 1,
std::multiplies<>()); // 120
int cnt = std::count(v.begin(), v.end(), 3); // 1copy, remove & unique
remove doesn't actually erase—it shifts non-matching elements forward and returns a new end iterator. Pair with .erase() for the erase-remove idiom. unique similarly compacts consecutive duplicates; sort first to dedupe fully.
std::vector<int> v = {1, 2, 3, 4, 5};
std::vector<int> dest;
std::copy(v.begin(), v.end(), std::back_inserter(dest));
auto end = std::remove(v.begin(), v.end(), 3); // erase-remove idiom
v.erase(end, v.end());
std::vector<int> u = {1, 1, 2, 2, 3};
u.erase(std::unique(u.begin(), u.end()), u.end()); // 1 2 3min, max & clamp
min/max return the smaller/larger of two values or an initializer list. minmax returns both as a pair. clamp (C++17) restricts a value to a range, replacing manual if/else bounds checks—useful for input validation and UI coordinates.
int a = 3, b = 7;
std::cout << std::max(a, b); // 7
std::cout << std::min(a, b); // 3
auto [mn, mx] = std::minmax({3, 1, 4, 1, 5}); // mn=1, mx=5
int score = 105;
int clamped = std::clamp(score, 0, 100); // 100Error Handling
Exceptions: try/catch
Throw exceptions by value, catch by const reference to avoid slicing. Catching std::exception catches all standard exceptions via the base class. Exceptions are for exceptional cases, not normal control flow—they have overhead when thrown.
#include <stdexcept>
try {
int x = 10, y = 0;
if (y == 0) throw std::runtime_error("division by zero");
std::cout << x / y;
} catch (const std::exception &e) {
std::cerr << "Error: " << e.what();
}Standard Exception Hierarchy
<stdexcept> provides standard exception types. logic_error is for programmer errors (detectable before runtime); runtime_error for unforeseen runtime conditions. Derive custom exceptions from std::runtime_error so they integrate with standard catch blocks.
#include <stdexcept>
throw std::runtime_error("runtime error");
throw std::logic_error("logic error");
throw std::out_of_range("index out of range");
throw std::invalid_argument("bad argument");
throw std::bad_alloc(); // thrown by new on allocation failure
// std::exception is the base; .what() returns the messageCustom Exceptions
Derive custom exceptions from a standard base so they integrate with catch(const std::exception&). Add context fields (file paths, error codes) that help debugging. Always pass the message to the base constructor so what() works.
class FileError : public std::runtime_error {
public:
FileError(const std::string &msg, const std::string &path)
: std::runtime_error(msg + ": " + path), path_(path) {}
const std::string &path() const { return path_; }
private:
std::string path_;
};
try { throw FileError("not found", "data.txt"); }
catch (const FileError &e) {
std::cerr << e.what() << " at " << e.path();
}noexcept & RAII Safety
noexcept promises a function won't throw, enabling compiler optimizations. If it does throw, std::terminate is called. RAII guarantees destructors run during stack unwinding, so resources are freed even when exceptions propagate up the call stack.
void safe_op() noexcept {
// guaranteed not to throw
}
void risky() {
std::vector<int> v(1000); // RAII: cleanup on exception
throw std::runtime_error("oops");
// v's destructor still runs during stack unwinding
}Assertions
assert() checks conditions in debug builds; it's removed in release (when NDEBUG is defined) so don't use it for production checks. Use it for internal invariants that indicate bugs. For user-facing validation, throw exceptions or return error codes.
#include <cassert>
double sqrt_safe(double x) {
assert(x >= 0 && "sqrt of negative");
return std::sqrt(x);
}
// In release builds (NDEBUG defined), assert is removedFile I/O & Streams
Reading a File
ifstream opens a file for reading. Always check if the open succeeded (!file evaluates true on failure). getline reads line by line including spaces. The stream's destructor closes the file automatically (RAII), so no manual close needed.
#include <fstream>
#include <string>
std::ifstream file("input.txt");
if (!file) { std::cerr << "cannot open"; return 1; }
std::string line;
while (std::getline(file, line)) {
std::cout << line << "\n";
}Writing a File
ofstream writes to a file, truncating by default. Use std::ios::app to append, std::ios::binary for binary mode. The << operator works exactly like std::cout. Flush with out.flush() or use std::endl (which also flushes).
#include <fstream>
std::ofstream out("output.txt");
if (!out) return 1;
out << "Line 1\n";
out << "Value: " << 42 << "\n";
out.close(); // optional, destructor closes
// Append mode: std::ofstream("f.txt", std::ios::app);String Streams
ostringstream builds strings from mixed types (like a buffer). istringstream parses strings into typed values. They're slower than direct string operations but very convenient for serialization, URL building, and token parsing.
#include <sstream>
// Build a string from mixed types
std::ostringstream oss;
oss << "Name=" << "Alice" << "&Age=" << 30;
std::string url = oss.str();
// Parse tokens from a string
std::istringstream iss("10 20 30");
int a, b, c;
iss >> a >> b >> c;Binary Files
Binary mode avoids newline translation and is more compact than text. write/read take char* and byte count—use reinterpret_cast for structs. Note: binary files are not portable across architectures (endianness, struct padding differ).
#include <fstream>
struct Record { int id; double value; };
// Write
std::ofstream out("data.bin", std::ios::binary);
Record r{1, 3.14};
out.write(reinterpret_cast<char*>(&r), sizeof(r));
// Read
std::ifstream in("data.bin", std::ios::binary);
Record r2;
in.read(reinterpret_cast<char*>(&r2), sizeof(r2));Formatted Output (C++20 fmt)
std::format (C++20) brings Python-style format strings to C++, replacing messy iomanip manipulators. For older code, <iomanip> provides setprecision, setw, setfill. The {fmt} library is a popular pre-C++20 alternative with the same syntax.
#include <format> // C++20
std::string s = std::format("Hello, {}! You are {}.", "Alice", 30);
std::cout << std::format("{:.2f}", 3.14159); // 3.14
std::cout << std::format("{:>10}", "right"); // padded
// Pre-C++20: use iomanip
#include <iomanip>
std::cout << std::fixed << std::setprecision(2) << 3.14159;Smart Pointers
unique_ptr - Exclusive Ownership
unique_ptr is the default smart pointer—use it when one owner is enough. It has zero overhead vs raw pointers. make_unique is preferred (exception-safe). Cannot copy, only move. Custom deleters enable RAII for C resources like FILE* or sockets.
#include <memory>
std::unique_ptr<int> p1 = std::make_unique<int>(42);
// std::unique_ptr<int> p2 = p1; // ERROR: cannot copy
std::unique_ptr<int> p2 = std::move(p1); // OK: transfer ownership
// p1 is now nullptr
// Custom deleter
auto deleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(deleter)> fp(fopen("f.txt", "r"), deleter);shared_ptr - Shared Ownership
shared_ptr uses reference counting—multiple pointers can own the same object. Object is destroyed when refcount hits 0. make_shared is preferred (single allocation for object + control block). Heavier than unique_ptr due to atomic refcount and control block. Use when ownership is genuinely shared.
#include <memory>
auto p1 = std::make_shared<int>(42);
auto p2 = p1; // OK: both share ownership
std::cout << *p1 << " " << p1.use_count(); // 42 2
// Control block holds refcount + deleter + allocator
// Refcount is atomic (thread-safe), but object access is NOT
std::shared_ptr<int> p3{new int{10}}; // uses non-array new
// Thread-safe: refcount operations are atomic
// NOT thread-safe: accessing the pointed-to objectweak_ptr - Breaking Cycles
weak_ptr is a non-owning observer of a shared_ptr. Doesn't increase refcount. Use lock() to temporarily get a shared_ptr (returns null if object was destroyed). Essential for breaking reference cycles (e.g., doubly-linked lists, parent-child relationships) that would cause memory leaks.
#include <memory>
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // weak to avoid cycle
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a; // weak_ptr from shared_ptr
// To use a weak_ptr, lock() into a shared_ptr
if (auto locked = b->prev.lock()) {
std::cout << "prev exists";
} else {
std::cout << "prev expired";
}Smart Pointers with Arrays
Smart pointers can manage arrays. unique_ptr<T[]> provides operator[] and correct delete[]. shared_ptr<T[]> is supported since C++17. However, std::vector or std::array are almost always better—safer, more ergonomic, and self-documenting. Use smart array pointers only when interfacing with legacy APIs.
#include <memory>
// C++17: shared_ptr supports arrays
std::shared_ptr<int[]> arr1(new int[10]);
arr1[0] = 42;
// unique_ptr with arrays (partial specialization exists)
std::unique_ptr<int[]> arr2 = std::make_unique<int[]>(10);
arr2[5] = 100;
// Prefer std::array or std::vector over raw arrays
#include <vector>
std::vector<int> v(10); // better choiceenable_shared_from_this
When an object needs to return a shared_ptr to itself, enable_shared_from_this provides safe shared_from_this(). Calling shared_ptr<T>(this) directly would create a second control block, leading to double-free. The object must already be managed by a shared_ptr, or shared_from_this() throws bad_weak_ptr.
#include <memory>
class Widget : public std::enable_shared_from_this<Widget> {
public:
std::shared_ptr<Widget> getPtr() {
return shared_from_this(); // safe
// return shared_ptr<Widget>(this); // BAD: double delete
}
};
auto w = std::make_shared<Widget>();
auto w2 = w->getPtr(); // shares ownership, refcount = 2Move Semantics & Rvalue References
Lvalues, Rvalues, and References
Lvalues have identity and persist beyond a single expression (named objects). Rvalues are temporary or literal values. T& binds to lvalues, T&& binds to rvalues. const T& is special—it binds to both. Understanding this distinction is the foundation of move semantics.
int x = 10; // x is an lvalue
int& lref = x; // lvalue reference
int&& rref = 20; // rvalue reference (binds to temporary)
// int& bad = 20; // ERROR: can't bind lref to rvalue
const int& cref = 20; // OK: const lref binds to rvalue
int y = x + 5; // (x + 5) is an rvalue (prvalue)
std::string s1 = "hi";
std::string&& rr = std::move(s1); // rr is named rvaluestd::move and Move Constructors
std::move doesn't move anything—it casts to rvalue, enabling move constructor/assignment to be selected. Move operations should be noexcept so containers can use them during reallocation (otherwise they fall back to copy for exception safety). After a move, the source object is in a valid-but-unspecified state.
class Buffer {
int* data;
size_t size;
public:
// Move constructor: steal resources
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}
// Move assignment
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data; size = other.size;
other.data = nullptr; other.size = 0;
}
return *this;
}
};
Buffer a(100);
Buffer b = std::move(a); // calls move ctor, a is now emptyPerfect Forwarding
Perfect forwarding passes arguments to another function while preserving their value category (lvalue vs rvalue). T&& in a deduced context is a 'forwarding reference' (not rvalue reference). std::forward<T> conditionally casts: lvalue if T is T&, rvalue if T is T&&. Essential for factory functions and wrappers.
#include <utility>
template <typename T, typename Arg>
auto make_unique(Arg&& arg) {
return std::unique_ptr<T>(new T(std::forward<Arg>(arg)));
}
template <typename... Args>
void log(Args&&... args) {
// std::forward preserves value category
log_impl(std::forward<Args>(args)...);
}
std::string s = "hello";
log(s); // Arg = std::string& (lvalue)
log(std::string()); // Arg = std::string&& (rvalue)Rule of Five / Rule of Zero
Rule of Five: if a class manages a resource, you must define destructor, copy ctor, copy assign, move ctor, move assign. Rule of Zero: prefer composing RAII types (vector, string, smart pointers) so the compiler-generated special members are correct. This eliminates bug-prone manual resource management.
// Rule of Five: if you define any of these, define all 5
class Resource {
int* data;
public:
Resource(size_t n) : data(new int[n]) {}
~Resource() { delete[] data; }
Resource(const Resource& o); // copy ctor
Resource& operator=(const Resource& o); // copy assign
Resource(Resource&& o) noexcept; // move ctor
Resource& operator=(Resource&& o) noexcept; // move assign
};
// Rule of Zero: prefer to use RAII types
class Better {
std::vector<int> data; // handles everything
public:
Better() = default; // compiler-generated funcs are correct
};Return Value Optimization (RVO/NRVO)
RVO/NRVO allows the compiler to construct the return value directly in the caller's storage, avoiding copies/moves entirely. C++17 makes RVO mandatory for prvalues. Never write return std::move(local)—it inhibits NRVO and forces a (slower) move. Just return the local by name and let the compiler optimize.
std::vector<int> makeVec() {
std::vector<int> v;
v.push_back(1); v.push_back(2);
return v; // NRVO: no copy, no move!
}
std::string makeStr() {
return std::string("hello"); // RVO
}
// C++17 guarantees RVO (mandatory copy elision) for prvalues
std::vector<int> v = makeVec(); // constructed in place
// Don't std::move return values—it disables RVO!
std::vector<int> bad() {
std::vector<int> v;
return std::move(v); // WORSE: forces move, blocks NRVO
}Concurrency (thread, mutex, async)
std::thread Basics
std::thread launches a new OS thread. You MUST call join() (wait) or detach() (let it run independently) before the thread object is destroyed, else std::terminate is called. Arguments are passed by value by default—use std::ref for references, std::move for move-only types. Prefer join unless you have a clear reason to detach.
#include <thread>
#include <iostream>
void worker(int id) {
std::cout << "Thread " << id << "\n";
}
int main() {
std::thread t1(worker, 1);
std::thread t2([]{ std::cout << "Lambda thread\n"; });
t1.join(); // wait for t1 to finish
t2.join(); // must join or detach before destruction
// t1.detach(); // runs independently (risky)
// Hardware concurrency hint
unsigned n = std::thread::hardware_concurrency();
}Mutex and Lock Guard
Always protect shared data with a mutex. std::lock_guard is the simplest RAII lock—acquires on construction, releases on destruction. std::scoped_lock (C++17) safely locks multiple mutexes using a deadlock-avoidance algorithm. std::unique_lock offers more flexibility (manual lock/unlock, deferred locking) for use with condition variables.
#include <mutex>
#include <vector>
std::mutex mtx;
std::vector<int> shared;
void safePush(int val) {
// RAII lock: unlocks even if exception thrown
std::lock_guard<std::mutex> lock(mtx);
shared.push_back(val);
} // lock released here
// std::scoped_lock (C++17) locks multiple mutexes deadlock-free
std::mutex m1, m2;
void transfer() {
std::scoped_lock lock(m1, m2); // atomic
}
// std::unique_lock: lockable/unlockable, movable
std::unique_lock<std::mutex> ul(mtx);
ul.unlock();
ul.lock();Condition Variables
condition_variable lets threads wait for a condition. Always use a predicate with wait() to handle spurious wakeups. The mutex must be held by a unique_lock when calling wait(), which releases it while waiting and re-acquires before returning. notify_one wakes one waiter, notify_all wakes all. This pattern implements thread-safe queues and producer-consumer pipelines.
#include <condition_variable>
#include <queue>
#include <thread>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> tasks;
void producer() {
{
std::lock_guard<std::mutex> lock(mtx);
tasks.push(42);
}
cv.notify_one(); // wake one waiting consumer
}
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return !tasks.empty(); }); // predicate prevents spurious wakeup
int task = tasks.front();
tasks.pop();
}std::async and Futures
std::async is a high-level way to run tasks asynchronously, returning a future. std::launch::async forces a new thread; std::launch::deferred runs lazily on get(). Default policy may choose either—be explicit for predictable behavior. For more control, use std::promise/future pairs. Always call get() on a future before destruction, else the destructor may block.
#include <future>
#include <iostream>
int slowComputation() {
std::this_thread::sleep_for(std::chrono::seconds(1));
return 42;
}
// async policy: may run in new thread or deferred
auto fut = std::async(std::launch::async, slowComputation);
// do other work...
int result = fut.get(); // blocks until ready
// std::launch::async: definitely new thread
// std::launch::deferred: lazy, runs on get()
// std::launch::async | deferred: implementation chooses
// std::promise for manual control
std::promise<int> p;
std::future<int> f = p.get_future();
std::thread([&p]{ p.set_value(10); }).detach();
f.get(); // 10Atomic Operations
std::atomic provides lock-free thread-safe operations for primitive types. Heavier than a plain int but much lighter than mutex for simple counters/flags. Memory ordering affects visibility: relaxed (no ordering), acquire/release (pair for synchronization), seq_cst (default, strongest). Use atomics for counters/flags; use mutex for complex critical sections.
#include <atomic>
#include <iostream>
std::atomic<int> counter{0};
std::atomic<bool> ready{false};
void worker() {
while (!ready.load(std::memory_order_acquire));
counter.fetch_add(1, std::memory_order_relaxed);
}
// Compare-and-swap (CAS) loop
std::atomic<int> val{0};
int expected = 0;
bool success = val.compare_exchange_weak(
expected, 1,
std::memory_order_acq_rel);
// Atomic is lock-free for most primitive types
static_assert(std::atomic<int>::is_always_lock_free);
// std::atomic_flag: guaranteed lock-free, for spinlocks
std::atomic_flag spin = ATOMIC_FLAG_INIT;
while (spin.test_and_set(std::memory_order_acquire)); // acquire
spin.clear(std::memory_order_release); // releaseTemplate Metaprogramming
Template Specialization
Template specialization provides custom implementations for specific types. Full specialization fixes all template parameters. Partial specialization (only for class templates) specializes some parameters while keeping others generic. Used heavily in type traits, std::vector<bool>, and optimizing for known types.
// Primary template
template <typename T>
struct TypeName {
static const char* get() { return "unknown"; }
};
// Full specialization for int
template <>
struct TypeName<int> {
static const char* get() { return "int"; }
};
// Full specialization for const char*
template <>
struct TypeName<const char*> {
static const char* get() { return "string"; }
};
std::cout << TypeName<int>::get(); // "int"
std::cout << TypeName<double>::get(); // "unknown"
std::cout << TypeName<const char*>::get(); // "string"SFINAE and enable_if
SFINAE (Substitution Failure Is Not An Error) lets you enable/disable template overloads based on type properties. std::enable_if conditionally defines a type. When substitution fails, the overload is silently removed instead of causing an error. C++17's if constexpr and C++20 concepts often replace SFINAE with cleaner syntax.
#include <type_traits>
// SFINAE: Substitution Failure Is Not An Error
// Only enable this overload if T is integral
template <typename T,
typename = std::enable_if_t<std::is_integral_v<T>>>
void process(T x) { std::cout << "integral\n"; }
// Only enable if T is floating point
template <typename T,
std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
void process(T x) { std::cout << "floating\n"; }
process(10); // integral
process(3.14); // floating
// void_t trick (C++17) for detecting member existence
template <typename T, typename = void>
struct has_size : std::false_type {};
template <typename T>
struct has_size<T, std::void_t<decltype(std::declval<T>().size())>>
: std::true_type {};if constexpr (C++17)
if constexpr evaluates the condition at compile time and discards the false branch entirely (not even type-checked). This replaces many SFINAE patterns with much cleaner code. Particularly useful for template recursion (the base case terminates the recursion) and for branching on type traits without instantiation errors.
#include <type_traits>
template <typename T>
auto getValue(T t) {
if constexpr (std::is_pointer_v<T>) {
return *t; // only compiled if T is a pointer
} else if constexpr (std::is_integral_v<T>) {
return t * 2;
} else {
return t;
}
}
int x = 5;
int* p = &x;
getValue(x); // 10
getValue(p); // 5
getValue(3.14); // 3.14
// Compile-time recursion with termination
template <int N>
constexpr int factorial() {
if constexpr (N <= 1) return 1;
else return N * factorial<N - 1>();
}Variadic Templates and Fold Expressions
Variadic templates accept any number of arguments via parameter packs (typename... Args). C++17 fold expressions apply an operator to all pack elements: unary fold (... op pack), binary fold (init op ... op pack). Before C++17, you needed recursion with a base case. Variadic templates power std::make_unique, std::tuple, printf-like functions.
#include <iostream>
// Parameter pack
template <typename... Args>
void print(Args... args) {
// C++17 fold expression
((std::cout << args << " "), ...);
std::cout << "\n";
}
print(1, "hello", 3.14, 'x'); // 1 hello 3.14 x
// Sum with fold
template <typename... T>
auto sum(T... args) {
return (args + ...); // binary fold: ((a+b)+c)+d
}
// Sum with initial value
template <typename... T>
auto sumFrom0(T... args) {
return (0 + ... + args); // left fold with init
}
// Base case recursion (pre-C++17)
template <typename T>
void printOne(T t) { std::cout << t; }Concepts (C++20)
Concepts (C++20) replace SFINAE with readable, intention-revealing constraints. They produce much better error messages than SFINAE. Define concepts with concept Name = constraint;. Use them in template parameters, requires clauses, or abbreviated templates (auto with concept). The standard library provides many useful concepts in <concepts>.
#include <concepts>
// Define a concept
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
// Use as constraint
template <Numeric T>
T add(T a, T b) { return a + b; }
// Requires clause
template <typename T>
requires requires(T t) { t.size(); }
auto getSize(const T& t) { return t.size(); }
// Concepts in abbreviated function templates
void process(std::integral auto x) { /* ... */ }
void process(std::ranges::range auto& r) { /* ... */ }
// Standard concepts: integral, floating_point, equality_comparable,
// totally_ordered, copyable, movable, default_initializable, etc.Lambda Expressions Deep Dive
Capture Modes
Lambdas capture variables from the enclosing scope. [=] captures all by value, [&] by reference—convenient but error-prone (dangling references, unintended captures). Prefer explicit captures [x, &y] for clarity. Init captures [name = expr] (C++14) allow renaming, moving, and computing captured values. Capture [&] carefully—lambdas outliving the scope cause dangling references.
int x = 10, y = 20;
std::vector<int> v;
auto a = [] { /* no capture */ };
auto b = [x] { return x; }; // capture x by value
auto c = [&x] { x = 100; }; // capture x by reference
auto d = [=] { return x + y; }; // capture all by value
auto e = [&] { x = 1; y = 2; }; // capture all by reference
auto f = [x, &y] { return x + y; }; // mixed
auto g = [=, &x] { x = 1; return y; }; // default value, x by ref
auto h = [this] { return member; }; // capture this (C++17: *this)
auto i = [x = x + 5] { return x; }; // init capture (C++14)
// Best practice: capture only what you need, explicitlyGeneric Lambdas (C++14)
Generic lambdas use auto parameters (C++14) or explicit template parameters (C++20). They're essentially compiler-generated template operator() overloads. C++20 template lambdas let you access the type parameter T directly. Recursive lambdas need std::function (or C++23 deducing this) because a plain auto lambda can't refer to itself by name before its type is known.
// auto parameters (C++14)
auto add = [](auto a, auto b) { return a + b; };
add(1, 2); // int
add(1.5, 2.5); // double
add(std::string("a"), std::string("b"));
// Template lambdas (C++20)
auto getSize = []<typename T>(const std::vector<T>& v) {
return v.size();
};
// Perfect forwarding in lambdas (C++20)
auto wrapper = []<typename T>(T&& arg) {
return process(std::forward<T>(arg));
};
// Recursive lambda with std::function
#include <functional>
std::function<int(int)> fib = [&](int n) {
return n < 2 ? n : fib(n - 1) + fib(n - 2);
};Mutable and Stateful Lambdas
By default, value-captured variables in a lambda are const—call operator() is const. The mutable keyword removes this constness, allowing modification of captured-by-value variables (modifying the copy, not the original). This enables stateful lambdas like counters. Each copy of a lambda has its own captured state. Use std::function to store lambdas of different types in containers.
#include <functional>
int counter = 0;
// Without mutable, value-captured vars are const
auto inc = [counter]() mutable {
return ++counter; // modifies the captured copy
};
inc(); inc();
std::cout << counter; // still 0 (original unchanged)
// Stateful counter with std::function
auto makeCounter(int start) {
return [count = start]() mutable { return ++count; };
}
auto c = makeCounter(10);
c(); c(); // 11, 12
// Lambda as callback storage
std::vector<std::function<void()>> callbacks;
callbacks.push_back([&]{ std::cout << "click"; });Lambdas with STL Algorithms
Lambdas shine as predicates/comparators for STL algorithms. They replace the old functor (function object) approach with inline, readable code. Common uses: comparators for sort, predicates for find_if/remove_if/ copy_if, transformations for transform. C++20 ranges make this even cleaner with the pipe operator for composable transformations.
#include <algorithm>
#include <vector>
std::vector<int> v = {5, 2, 8, 1, 9, 3};
// Sort descending
std::sort(v.begin(), v.end(), [](int a, int b) {
return a > b;
});
// Find first even
auto it = std::find_if(v.begin(), v.end(),
[](int x) { return x % 2 == 0; });
// Remove odds (erase-remove idiom)
v.erase(std::remove_if(v.begin(), v.end(),
[](int x) { return x % 2 != 0; }), v.end());
// Transform
std::transform(v.begin(), v.end(), v.begin(),
[](int x) { return x * x; });
// C++20 ranges: cleaner
auto evens = v | std::views::filter([](int x){ return x%2==0; })
| std::views::transform([](int x){ return x*2; });Higher-Order Functions
Lambdas enable functional programming patterns in C++. Functions can return lambdas (closures) or accept them as parameters. std::function wraps any callable for type-erased storage. Function composition builds pipelines by chaining lambdas. This style is powerful for callbacks, event handlers, and data transformation pipelines, though template-based approaches avoid std::function overhead.
#include <functional>
// Function returning a function
auto makeMultiplier(int factor) {
return [factor](int x) { return x * factor; };
}
auto doubleIt = makeMultiplier(2);
auto tripleIt = makeMultiplier(3);
doubleIt(5); // 10
tripleIt(5); // 15
// Function taking a function
template <typename F>
void applyTwice(int x, F f) {
std::cout << f(f(x));
}
applyTwice(5, [](int x){ return x + 3; }); // 11
// Composition
auto compose = [](auto f, auto g) {
return [f, g](auto x) { return f(g(x)); };
};
auto addOne = [](int x){ return x + 1; };
auto square = [](int x){ return x * x; };
auto sqThenAdd = compose(addOne, square);
sqThenAdd(3); // 10Namespaces & Modules
Namespace Basics
Namespaces prevent name collisions in large projects. They can be nested and aliased for convenience. Anonymous namespaces give symbols internal linkage (visible only in the current translation unit), replacing the C-style static keyword for this purpose. Avoid using namespace in headers—it pollutes every including file.
namespace math {
double pi = 3.14159;
double square(double x) { return x * x; }
namespace geometry { // nested
double circleArea(double r) { return pi * square(r); }
}
}
// Usage
double a = math::square(5);
double area = math::geometry::circleArea(2.0);
// Namespace alias
namespace geo = math::geometry;
geo::circleArea(1.0);
// Anonymous namespace: internal linkage (like 'static')
namespace {
int internalVar = 42; // only visible in this file
}using Declarations and Directives
using declaration (using std::cout) imports a single name—safe and recommended. using directive (using namespace std) imports everything—convenient but risky, especially in headers (causes name conflicts and ambiguity). Keep using directives to small scope (function/implementation files) and never in headers. C++20 using enum imports all enumerators of a scoped enum.
#include <iostream>
#include <vector>
// using declaration: brings one name
using std::cout;
using std::vector;
cout << "hello";
vector<int> v;
// using directive: brings entire namespace
using namespace std; // brings ALL of std
// AVOID in headers! Pollutes global namespace.
// Namespace-scoped using (safe)
namespace mylib {
using std::string; // only affects mylib
using std::vector;
string s;
}
// C++20: using enum
enum class Color { Red, Green, Blue };
void print() {
using enum Color;
auto c = Red; // no Color:: needed here
}Argument-Dependent Lookup (ADL)
ADL (Koenig lookup) finds free functions based on the namespaces of their arguments. This is why std::cout << x works without std::operator<<—the compiler looks in x's namespace. ADL is essential for operator overloads and customization points like swap. The 'using std::swap; swap(a,b);' pattern lets user types provide optimized swaps while falling back to std::swap.
namespace mylib {
struct Widget {
int value;
};
// Operator overload found via ADL
std::ostream& operator<<(std::ostream& os, const Widget& w) {
return os << "Widget(" << w.value << ")";
}
void helper(const Widget&) {}
}
int main() {
mylib::Widget w{42};
// ADL: finds operator<< in mylib without qualification
std::cout << w; // works! no mylib:: needed
// ADL applies to free functions too
helper(w); // wait—needs mylib::helper unless ADL applies
mylib::helper(w); // explicit
}
// swap is the classic ADL use case
namespace ns { struct X {};
void swap(X&, X&) {} // customized swap
}
void f(ns::X& a, ns::X& b) {
using std::swap;
swap(a, b); // ADL picks ns::swap if available
}Inline Namespaces (Versioning)
Inline namespaces expose their members as if they were in the enclosing namespace. This enables library versioning: make the newest version inline so users get it by default, while old versions remain accessible via explicit qualification. It's also used for ABI compatibility and feature toggling. Changing which namespace is inline shifts the default version without modifying user code.
// Inline namespace members are part of the enclosing namespace
namespace mylib {
inline namespace v2 {
struct Widget { int x, y, z; }; // newer version
void process(Widget) {}
}
namespace v1 {
struct Widget { int x, y; }; // older version
}
}
// v2::Widget is accessible as mylib::Widget (inline)
mylib::Widget w; // actually mylib::v2::Widget
mylib::process(w);
// Explicitly use v1 if needed
mylib::v1::Widget oldW;
// ABI versioning: change which namespace is inline
// to change default version without breaking old codeC++20 Modules
C++20 modules replace #include with a faster, more robust system. export module declares a module; export marks visible declarations. Modules are compiled once (not re-parsed per translation unit), dramatically improving build times. They avoid macro pollution and header order issues. Adoption is gradual—toolchain support (CMake, build systems) is still maturing as of 2024.
// math.cppm (module interface unit)
export module math;
export double pi = 3.14159;
export double square(double x) {
return x * x;
}
// Internal (not exported)
double internalHelper(double x) {
return x * 2;
}
// main.cpp
import math;
import std; // standard library module (C++23)
int main() {
return square(pi); // 9.87
// internalHelper(3); // ERROR: not exported
}
// Module partitions (sub-modules)
export module math:geometry;
export double circleArea(double r);Preprocessor & Macros
Include Guards and #pragma once
Include guards prevent a header from being processed multiple times in one translation unit, avoiding redefinition errors. #ifndef/#define/#endif is standard and portable. #pragma once is simpler and avoids macro name collisions but is technically non-standard (supported by all major compilers). Modern code often uses #pragma once for simplicity.
// Traditional include guard (header.h)
#ifndef MY_HEADER_H
#define MY_HEADER_H
// declarations here
class Widget { /* ... */ };
#endif // MY_HEADER_H
// Alternative: #pragma once (non-standard but widely supported)
#pragma once
class Widget { /* ... */ };
// #pragma once pros: simpler, no risk of macro name collision
// Include guards pros: standard, works everywhere
// Both prevent multiple inclusion in a single TUFunction-like Macros
Function-like macros are text substitution. ALWAYS parenthesize each argument and the whole expression to avoid precedence bugs. The do { ... } while (0) idiom makes a macro behave like a single statement. Macros have no type checking, no scope, and can have side effects (MAX(i++, j++) increments twice). Prefer constexpr/inline/templates in modern C++.
// Basic macro
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))
int m = MAX(3, 5); // ((3) > (5) ? (3) : (5))
int s = SQUARE(4); // ((4) * (4))
// ALWAYS parenthesize arguments!
#define BAD(x) x * x
BAD(1 + 2); // 1 + 2 * 1 + 2 = 5, not 9!
// Multi-line macro with do-while
#define LOG(msg) do { \
std::cerr << __FILE__ << ":" << __LINE__ \
<< " " << msg << "\n"; \
} while (0)
// Variadic macro (C++11)
#define PRINT(...) printf(__VA_ARGS__)
PRINT("x=%d, y=%d\n", x, y);Stringification and Concatenation
# (stringification) converts a macro argument to a string literal. ## (token pasting) concatenates two tokens into one. The two-level STR/XSTR trick first expands macros then stringifies them. These operators are essential for code generation, logging macros, and X-macros. Modern C++ often replaces these with constexpr functions, but they remain useful for compile-time string/token generation.
// # (stringification) turns a macro argument into a string literal
#define STR(x) #x
#define XSTR(x) STR(x)
const char* s1 = STR(hello world); // "hello world"
const char* s2 = STR(42); // "42"
// Two-level macro to expand macros before stringifying
#define VERSION 100
const char* v1 = STR(VERSION); // "VERSION"
const char* v2 = XSTR(VERSION); // "100"
// ## (token pasting) concatenates tokens
#define CONCAT(a, b) a ## b
#define MAKE_VAR(n) var_ ## n
int MAKE_VAR(1) = 10; // int var_1 = 10;
int CONCAT(foo, bar) = 5; // int foobar = 5;
// Useful for generating unique names
#define UNIQUE(prefix) prefix ## __LINE__
int UNIQUE(tmp_) = 0; // int tmp_42 = 0; (if on line 42)Conditional Compilation
Conditional compilation (#if, #ifdef, #ifndef, #elif, #else, #endif) includes/excludes code at compile time. Used for platform-specific code, debug builds, and feature flags. defined(X) checks if X is defined (value irrelevant). #error aborts compilation with a message. Prefer constexpr if when possible—it's type-safe and the code is always compiled (catching errors in all configurations).
#define DEBUG 1
#define PLATFORM "windows"
#if DEBUG
std::cout << "Debug mode\n";
#endif
#ifdef DEBUG
logDebug("entered function");
#endif
#ifndef NDEBUG
assert(ptr != nullptr);
#endif
#if defined(_WIN32) || defined(_WIN64)
#include <windows.h>
#elif defined(__linux__)
#include <unistd.h>
#elif defined(__APPLE__)
#include <TargetConditionals.h>
#endif
// Check C++ standard
#if __cplusplus >= 202002L
#define CPP20_OR_LATER
#elif __cplusplus >= 201703L
#define CPP17_OR_LATER
#endif
// #error and #warning
#if !defined(VERSION)
#error "VERSION must be defined"
#endifPredefined Macros and __VA_OPT__
Predefined macros provide compile-time info: __FILE__/__LINE__ for logging, __cplusplus for standard version, __func__ for function names (it's an identifier, not a macro). __VA_OPT__ (C++20) conditionally inserts a comma only when __VA_ARGS__ is non-empty, fixing the trailing comma problem in variadic macros. __has_include (C++17) enables optional header inclusion for portability.
// Standard predefined macros
std::cout << __FILE__; // source file path
std::cout << __LINE__; // current line number
std::cout << __DATE__; // compilation date "Mmm dd yyyy"
std::cout << __TIME__; // compilation time "hh:mm:ss"
std::cout << __cplusplus; // C++ standard version
std::cout << __func__; // current function name (not macro)
// Compiler-specific
#ifdef __GNUC__
std::cout << __GNUC__ << "." << __GNUC_MINOR__;
#endif
#ifdef _MSC_VER
std::cout << "MSVC " << _MSC_VER;
#endif
// __VA_OPT__ (C++20): expands to its arg if variadic has args
#define LOG(fmt, ...) \
printf(fmt __VA_OPT__(,) __VA_ARGS__)
LOG("plain"); // printf("plain")
LOG("x=%d", x); // printf("x=%d", x)
// __has_include (C++17): check if header exists
#if __has_include(<optional>)
#include <optional>
#endifDesign Patterns in C++
Singleton (Meyers' Singleton)
Singleton ensures a class has one instance with global access. Meyers' Singleton (static local variable) is the cleanest C++ implementation—thread-safe initialization is guaranteed since C++11. Delete copy operations to prevent duplication. Singletons are controversial (global state, hard to test); prefer dependency injection when feasible. Use when genuinely one instance is needed (logger, config, hardware interface).
class Logger {
public:
static Logger& instance() {
// Meyers' singleton: thread-safe in C++11+
static Logger inst;
return inst;
}
void log(const std::string& msg) {
std::cout << "[LOG] " << msg << "\n";
}
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
private:
Logger() = default;
};
// Usage
Logger::instance().log("started");
// Avoid: lazy init with new + manual sync (error-prone)
// The static local is initialized once, thread-safelyRAII (Resource Acquisition Is Initialization)
RAII is C++'s most important idiom: acquire resources in constructors, release in destructors. This guarantees cleanup even when exceptions are thrown. Smart pointers, std::lock_guard, std::fstream, and std::vector all use RAII. The destructor runs during stack unwinding, making exception-safe code natural. RAII eliminates manual new/delete, lock/unlock, open/close—embrace it everywhere.
// RAII: resource tied to object lifetime
class FileHandle {
FILE* fp;
public:
explicit FileHandle(const char* path) : fp(fopen(path, "r")) {
if (!fp) throw std::runtime_error("open failed");
}
~FileHandle() { if (fp) fclose(fp); }
FILE* get() { return fp; }
// disable copy to prevent double-close
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// allow move
FileHandle(FileHandle&& o) noexcept : fp(o.fp) { o.fp = nullptr; }
};
{
FileHandle f("data.txt");
// use f.get()
} // fclose called automatically, even on exceptionPimpl Idiom (Pointer to Implementation)
Pimpl (Pointer to Implementation) hides implementation details behind a pointer. Benefits: faster compilation (private members don't appear in header), true ABI stability (changing Impl doesn't break ABI), reduced include dependencies. The destructor must be defined in the .cpp file because unique_ptr<Impl> needs Impl to be complete. Common in library design for stable APIs.
// widget.h - public interface
class Widget {
public:
Widget();
~Widget(); // must define in .cpp (unique_ptr needs complete type)
Widget(Widget&&) noexcept;
Widget& operator=(Widget&&) noexcept;
void doSomething();
private:
class Impl; // forward declaration
std::unique_ptr<Impl> pimpl;
};
// widget.cpp - implementation
#include "widget.h"
#include <vector>
class Widget::Impl {
public:
std::vector<int> data;
void doSomething() { /* ... */ }
};
Widget::Widget() : pimpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;
Widget::Widget(Widget&&) noexcept = default;
void Widget::doSomething() { pimpl->doSomething(); }Observer Pattern
Observer lets subjects notify subscribers of changes without coupling. std::function makes it easy to accept lambdas, member functions, and functors. For production code, consider a token-based unsubscribe mechanism (return an ID on subscribe, use it to remove). Watch for dangling references if observers capture [&] and outlive the subject. Qt's signals/slots and C# events are mature implementations.
#include <functional>
#include <vector>
#include <string>
class Subject {
std::vector<std::function<void(const std::string&)>> observers;
public:
void subscribe(std::function<void(const std::string&)> cb) {
observers.push_back(cb);
}
void notify(const std::string& event) {
for (auto& cb : observers) cb(event);
}
};
// Usage
Subject s;
s.subscribe([](const std::string& e){ std::cout << "A: " << e; });
s.subscribe([](const std::string& e){ std::cout << "B: " << e; });
s.notify("hello"); // both callbacks invoked
// For removal, use a token/ID system or observer objects
// instead of std::functionCRTP (Curiously Recurring Template Pattern)
CRTP (Derived : Base<Derived>) achieves static polymorphism—the base class customizes behavior via the derived type. No virtual function overhead. Used to add functionality (Comparable, Iterable) to derived classes via mixins. Downcasting via static_cast is safe because the template guarantees the derived type. CRTP powers std::enable_shared_from_this, std::iterator, and many policy-based designs.
// CRTP: class Derived : public Base<Derived>
template <typename Derived>
struct Comparable {
bool operator==(const Derived& other) const {
return static_cast<const Derived*>(this)->equalTo(other);
}
bool operator!=(const Derived& other) const {
return !(*this == other);
}
};
struct Point : Comparable<Point> {
int x, y;
bool equalTo(const Point& o) const { return x == o.x && y == o.y; }
};
Point a{1, 2}, b{1, 2};
a == b; // true (uses Comparable::operator==)
a != b; // false
// Static polymorphism (no virtual overhead)
template <typename T>
void draw(const T& shape) {
static_cast<const T&>(shape).drawImpl();
}Smart Pointers Deep
unique_ptr
unique_ptr is sole owner of its object. Cannot be copied, only moved. Automatically deletes when out of scope. make_unique is the preferred way to create. Zero overhead vs raw pointers.
#include <memory>
std::unique_ptr<int> p1 = std::make_unique<int>(42);
// std::unique_ptr<int> p2 = p1; // Error: cannot copy
std::unique_ptr<int> p3 = std::move(p1); // OK: transfer ownership
// p1 is now nullptrshared_ptr
shared_ptr allows multiple owners via reference counting. use_count() shows the number of owners. Thread-safe for the counter but not the object. Use make_shared for efficiency (single allocation).
auto p1 = std::make_shared<int>(42);
auto p2 = p1; // OK: shared ownership
std::cout << p1.use_count(); // 2
// Reference counting: deleted when count reaches 0
// Thread-safe for reference count, not for the objectweak_ptr
weak_ptr is a non-owning reference to shared_ptr. Prevents circular references (memory leaks). lock() tries to convert to shared_ptr. Use expired() to check if the object still exists. Does not affect reference count.
auto shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared; // Does not increase count
if (auto locked = weak.lock()) { // Try to get shared_ptr
std::cout << *locked; // Use the object
} else {
std::cout << "Object deleted";
}Custom Deleter
Custom deleters allow unique_ptr to manage non-memory resources like file handles, sockets, and C APIs. The deleter type is part of the unique_ptr type. Useful for RAII with C libraries.
std::unique_ptr<FILE, decltype(&fclose)> file(fopen("test.txt", "r"), fclose);
// Or with lambda
auto deleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(deleter)> file2(fopen("test.txt", "r"), deleter);enable_shared_from_this
enable_shared_from_this allows an object to safely get a shared_ptr to itself. Calling shared_ptr(this) directly would create a second reference count, causing double deletion. Inherit from enable_shared_from_this and use shared_from_this().
class Node : public std::enable_shared_from_this<Node> {
public:
std::shared_ptr<Node> getPtr() {
return shared_from_this(); // Safe
// return std::shared_ptr<Node>(this); // BUG: double delete
}
};Move Semantics Deep
Move Constructor
Move constructors steal resources instead of copying. noexcept is important: STL containers only move (not copy) if the move is noexcept. Always leave the moved-from object in a valid state.
class Buffer {
int* data; size_t size;
public:
Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr; other.size = 0; // Leave in valid state
}
Buffer& operator=(Buffer&& other) noexcept {
delete[] data;
data = other.data; size = other.size;
other.data = nullptr; other.size = 0;
return *this;
}
};std::move
std::move is a cast to rvalue reference. It does not perform the move itself. The move constructor or assignment operator does the actual work. After std::move, the source object is in a valid but unspecified state.
std::string s1 = "Hello";
std::string s2 = std::move(s1); // s1 is now empty
// std::move does not move anything!
// It casts to an rvalue reference, enabling move
// The actual move happens in the constructor/assignmentPerfect Forwarding
Perfect forwarding preserves the value category of arguments. T&& in a template is a forwarding reference, binding to both lvalues and rvalues. std::forward<T> casts back to the original category. Essential for factory functions and wrappers.
template<typename T, typename Arg>
auto make(Arg&& arg) {
return std::make_shared<T>(std::forward<Arg>(arg));
}
// std::forward preserves value category:
// lvalue stays lvalue, rvalue stays rvalue
// Arg&& is a forwarding reference (not rvalue reference)RVO & NRVO
RVO (Return Value Optimization) and NRVO (Named RVO) eliminate copies by constructing the object in place. Compilers perform this automatically. Using std::move on a local return value prevents NRVO and may pessimize. Trust the compiler.
std::string create() {
std::string s = "Hello";
return s; // NRVO: no copy, no move
}
std::string s = create(); // RVO: no copy
// With -O2, compilers eliminate the copy/move entirely
// Do NOT use std::move on return of local variable!Rule of Five
Rule of Five: if you define any of destructor, copy constructor/assignment, or move constructor/assignment, define all five. This ensures correct resource management. The Rule of Zero is preferred: use RAII types (smart pointers, vectors) to avoid manual management.
class Resource {
public:
Resource(); // Constructor
~Resource(); // Destructor
Resource(const Resource&); // Copy constructor
Resource& operator=(const Resource&); // Copy assignment
Resource(Resource&&) noexcept; // Move constructor
Resource& operator=(Resource&&) noexcept; // Move assignment
};Templates Deep
Variadic Templates
Variadic templates accept any number of arguments. Fold expressions (C++17) simplify unpacking. The recursive approach works in C++11. sizeof...(args) gives the count. Used in tuple, make_shared, and printf replacements.
template<typename... Args>
void print(Args... args) {
(std::cout << ... << args) << '\n'; // C++17 fold expression
}
print(1, "hello", 3.14, 'a'); // 1hello3.14a
// Recursive unpacking (pre-C++17)
template<typename T, typename... Rest>
void print(T first, Rest... rest) {
std::cout << first;
print(rest...);
}SFINAE
SFINAE (Substitution Failure Is Not An Error) removes invalid overloads from consideration. enable_if adds a condition to the template. If the condition is false, the specialization is ignored. C++17 if constexpr is often cleaner.
template<typename T,
typename = std::enable_if_t<std::is_integral_v<T>>>
void process(T value) { /* integer version */ }
template<typename T,
typename = std::enable_if_t<std::is_floating_point_v<T>>,
typename = void> // Different signature
void process(T value) { /* float version */ }if constexpr
if constexpr (C++17) evaluates conditions at compile time. Only the true branch is compiled, the other is discarded. Much cleaner than SFINAE for conditional compilation. Works with auto return type deduction.
template<typename T>
auto get_value(T t) {
if constexpr (std::is_pointer_v<T>)
return *t;
else
return t;
}
// Only the matching branch is compiled
// No SFINAE neededConcepts (C++20)
Concepts (C++20) constrain template parameters with readable syntax. They provide better error messages than SFINAE. Use existing concepts (integral, floating_point) or define custom ones. Concepts can be combined with && and ||.
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template<Number T>
T add(T a, T b) { return a + b; }
// Or: requires clause
template<typename T> requires Number<T>
T multiply(T a, T b) { return a * b; }Template Specialization
Full specialization provides a complete implementation for a specific type. Partial specialization customizes for a category (e.g., all pointers). The primary template must be declared first. Specializations must match the interface.
template<typename T>
class Vector { /* general implementation */ };
template<>
class Vector<bool> { // Full specialization
// Bit-packed implementation
};
template<typename T>
class Vector<T*> { // Partial specialization for pointers
// Pointer-specific implementation
};STL Algorithms Deep
sort & stable_sort
sort is O(n log n), not stable. stable_sort preserves relative order of equal elements. Use a comparator for custom ordering. C++20 ranges allow: std::ranges::sort(v, {}, &last_digit).
std::vector<int> v = {3, 1, 4, 1, 5, 9};
std::sort(v.begin(), v.end()); // 1,1,3,4,5,9
std::sort(v.begin(), v.end(), std::greater<>()); // Descending
// Custom comparator
std::sort(v.begin(), v.end(), [](int a, int b) {
return a % 10 < b % 10; // Sort by last digit
});transform & accumulate
transform applies a function to each element. accumulate folds elements with an operation (default +). The initial value determines the type. Use std::reduce (C++17) for parallel reduction.
std::vector<int> v = {1, 2, 3, 4};
std::vector<int> squared(v.size());
std::transform(v.begin(), v.end(), squared.begin(),
[](int x) { return x * x; }); // 1,4,9,16
int sum = std::accumulate(v.begin(), v.end(), 0); // 10
int product = std::accumulate(v.begin(), v.end(), 1, std::multiplies<>());find & count
find returns an iterator to the first match or end(). count returns the number of matches. find_if uses a predicate. All are O(n). For sorted ranges, use binary_search, lower_bound, upper_bound (O(log n)).
std::vector<int> v = {1, 2, 3, 2, 1};
auto it = std::find(v.begin(), v.end(), 2); // First 2
size_t cnt = std::count(v.begin(), v.end(), 2); // 2
auto it2 = std::find_if(v.begin(), v.end(),
[](int x) { return x > 2; }); // First > 2copy & remove
copy_if copies matching elements. back_inserter appends to the destination. remove does not actually remove; it shifts non-matching elements forward and returns a new end. erase removes the leftover. C++20 adds std::erase for containers.
std::vector<int> v = {1, 2, 3, 4, 5};
std::vector<int> evens;
std::copy_if(v.begin(), v.end(), std::back_inserter(evens),
[](int x) { return x % 2 == 0; }); // 2,4
// Remove-erase idiom
v.erase(std::remove(v.begin(), v.end(), 3), v.end());
// C++20: std::erase(v, 3);minmax & clamp
minmax returns a pair of (min, max) in one pass. clamp restricts a value to a range. min/max accept initializer lists or two values. All accept custom comparators. Structured bindings (C++17) simplify the result.
auto [min, max] = std::minmax({3, 1, 4, 1, 5}); // C++17
int value = std::clamp(15, 0, 10); // 10 (clamped to max)
int m = std::min({1, 2, 3});
int m2 = std::max({1, 2, 3}, [](int a, int b) { return a < b; });Concurrency
std::thread
std::thread creates OS threads. join() waits for completion (blocking). detach() runs independently (may outlive the creator). A thread with neither causes std::terminate. Pass arguments by value or use std::ref for references.
#include <thread>
void task(int n) { /* ... */ }
std::thread t1(task, 42);
std::thread t2([]() { /* lambda */ });
t1.join(); // Wait for completion
t2.detach(); // Run independently
// Always join or detach before destructionstd::mutex
lock_guard is simple RAII: locks on construction, unlocks on destruction. unique_lock is more flexible: can unlock/relock, used with condition variables. Never unlock manually with lock_guard. Use std::scoped_lock for multiple mutexes.
std::mutex mtx;
int shared = 0;
void increment() {
std::lock_guard<std::mutex> lock(mtx); // RAII
++shared;
} // Auto-unlock
// std::unique_lock for conditional locking
std::unique_lock<std::mutex> ulock(mtx);
ulock.unlock(); // Manual unlock
ulock.lock(); // Re-lockstd::async & futures
std::async runs a function asynchronously, returning a future. get() blocks and retrieves the result. launch::async forces a new thread. launch::deferred runs synchronously on get(). Default may be either. Exceptions are propagated through get().
#include <future>
std::future<int> f = std::async(std::launch::async, []() {
return 42;
});
int result = f.get(); // Blocks until ready
// std::launch::async: always new thread
// std::launch::deferred: lazy, runs on get()Condition Variable
condition_variable synchronizes threads. wait releases the lock and blocks until notified. The predicate handles spurious wakeups. notify_one wakes one waiter, notify_all wakes all. Always modify shared state under the mutex before notifying.
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
// Waiter
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []() { return ready; }); // Spurious wakeups handled
// Notifier
{
std::lock_guard<std::mutex> lk(mtx);
ready = true;
}
cv.notify_one(); // Or notify_all()Atomic Operations
atomic provides lock-free thread-safe operations. fetch_add/sub are read-modify-write. compare_exchange implements CAS (compare-and-swap). memory_order_relaxed is fastest but weakest. seq_cst (default) is strongest. Use atomics for simple counters and flags.
#include <atomic>
std::atomic<int> counter{0};
counter++; // Atomic increment
counter.fetch_add(1, std::memory_order_relaxed);
bool expected = false;
counter.compare_exchange_strong(expected, true);
// Memory orders: relaxed, acquire, release, seq_cstModern C++ Features
Structured Bindings
Structured bindings (C++17) decompose pairs, tuples, and structs. auto& for references, auto for copies. Simplifies iteration over maps. Works with any aggregate type. Much cleaner than .first/.second.
std::pair p = {1, "hello"};
auto [num, str] = p; // C++17
std::map<std::string, int> m = {{"a", 1}, {"b", 2}};
for (const auto& [key, value] : m) {
std::cout << key << ": " << value;
}std::optional
optional represents a value that may or may not exist. Better than pointers or sentinel values. has_value() or operator bool checks. value() throws if empty, value_or() provides a default. Use for functions that may fail to produce a result.
#include <optional>
std::optional<int> find(bool found) {
if (found) return 42;
return std::nullopt;
}
auto result = find(true);
if (result) std::cout << *result;
// Or: result.value_or(0)std::variant
variant is a type-safe union. Holds one of several types. visit applies a visitor (overloaded lambda). index() returns the current type index. get_if<T> safely retrieves. Replaces unions and inheritance for closed type hierarchies.
#include <variant>
std::variant<int, std::string> v;
v = 42;
v = "hello";
std::visit([](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, int>)
std::cout << "int: " << arg;
else if constexpr (std::is_same_v<T, std::string>)
std::cout << "string: " << arg;
}, v);Ranges (C++20)
Ranges (C++20) provide composable, lazy views. The | operator chains operations. No intermediate containers. Views are lazy: computation happens on iteration. Much more readable than nested algorithm calls.
#include <ranges>
namespace rv = std::ranges::views;
auto result = std::vector{1, 2, 3, 4, 5}
| rv::filter([](int x) { return x % 2 == 0; })
| rv::transform([](int x) { return x * x; });
// Lazy evaluation: no computation until iterated
for (int x : result) std::cout << x; // 4, 16Coroutines (C++20)
Coroutines (C++20) enable async and generator patterns. co_yield suspends and returns a value. co_await waits for another coroutine. co_return finishes. The compiler transforms coroutines into state machines. Need a return type (Generator, Task) implementation.
#include <coroutine>
Generator<int> counter() {
for (int i = 0; ; ++i)
co_yield i; // Suspend and yield
}
for (int x : counter()) {
if (x > 5) break;
std::cout << x;
}
// co_await: wait for async operation
// co_return: finish coroutineCommon Pitfalls
Dangling Pointers
Dangling pointers point to freed memory. Accessing them is undefined behavior. Use smart pointers (unique_ptr, shared_ptr) to avoid manual memory management. If using raw pointers, set to nullptr after delete. Use tools like AddressSanitizer to detect.
// BUG: dangling pointer
int* p = new int(42);
delete p;
std::cout << *p; // Undefined behavior
// FIX: use smart pointers
auto p = std::make_unique<int>(42);
// Or set to nullptr after delete
int* p2 = new int(42);
delete p2; p2 = nullptr;Iterator Invalidation
Vector push_back may reallocate, invalidating all iterators. erase invalidates iterators at and after the point. list iterators are stable except for erased elements. Check iterator invalidation rules for each container. When in doubt, use indices.
std::vector<int> v = {1, 2, 3};
// BUG: iterator invalidated
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it == 2) v.push_back(4); // May invalidate it
}
// FIX: use index
for (size_t i = 0; i < v.size(); ++i) {
if (v[i] == 2) v.push_back(4);
}Undefined Behavior
Undefined behavior (UB) allows the compiler to do anything. Uninitialized variables, out-of-bounds access, null dereference, signed overflow are all UB. Compilers optimize assuming no UB, leading to surprising results. Use -fsanitize=undefined to detect.
int x; // Uninitialized: UB to read
std::cout << x; // UB
int arr[5];
arr[5] = 0; // Out of bounds: UB
int* p = nullptr;
*p; // Null dereference: UB
// Signed overflow: UB (unsigned wraps)
int big = INT_MAX + 1; // UBSlicing
Object slicing occurs when a derived object is copied to a base type. The derived parts are lost. Pass by reference or pointer to avoid slicing. Virtual calls on a sliced object call the base version. Always pass polymorphic types by reference/pointer.
class Base { public: virtual ~Base() {} };
class Derived : public Base { int extra; };
void take(Base b) { /* copies Base part only */ }
Derived d;
take(d); // Sliced: extra lost
// FIX: pass by reference or pointer
void take(const Base& b) { /* full object */ }Static Initialization
Static initialization order across translation units is undefined. One file may use another file not-yet-initialized static. Fix with function-local statics (Meyers Singleton): initialization is lazy and thread-safe (C++11+). Access via function call.
// Static initialization order fiasco
// File1.cpp
int x = computeX(); // May use y
// File2.cpp
int y = computeY(); // May use x
// Order across files is undefined!
// FIX: function-local static
int& getX() {
static int x = computeX(); // Lazy, thread-safe
return x;
}Related C++ snippets
Copy-paste ready code for common tasks.
Smart Pointers
unique_ptr, shared_ptr, weak_ptr.
RAII
Resource Acquisition Is Initialization.
Move Semantics
Rvalue references and move constructors.
Lambda Expressions
Lambda and captures.
Templates
Function templates and class templates.
STL Containers
Common container operations.
STL Algorithms
Common algorithm functions.
Iterator
Iterator types and usage.
Exception Handling
try-catch and custom exceptions.
Multithreading
thread, mutex, condition_variable.
File IO
File read and write operations.
Strings
std::string operations.
Regular Expressions
std::regex matching and replacement.
Type Deduction
auto, decltype, template deduction.
constexpr
Compile-time constants and computation.
Was this helpful?