../

Fundamentals

Core modern C++ (C++23, with the C++17/20 features it builds on) for someone who already writes TypeScript or Python: the toolchain, types, value semantics, references and pointers, RAII, classes, move semantics, templates, the standard library and the undefined behavior that bites. Compare with Python fundamentals and TypeScript fundamentals.

Mental model

The single biggest shift from TypeScript or Python: variables are objects, not references to objects. Assignment copies, the compiler decides memory layout at build time, and nothing is garbage-collected.

IdeaTypeScript / PythonC++
Variablename bound to a heap objectthe object itself, usually on the stack
b = aboth names share one objectb is an independent copy (unless a is a reference or pointer)
Memorygarbage collectorscope-based: destructors run at } (RAII)
Typeschecked (TS) or ignored (Py) at runtimefixed at compile time; no runtime type info by default
Errorsexceptions, runtime checks everywhereexceptions or std::expected; most misuse is undefined behavior, not an error
Genericserased (TS) / duck typing (Py)templates: a separate compiled copy per type
Buildinterpreter / bundlerpreprocess → compile each .cpp → link into a native binary
"Zero-cost"noyou do not pay at runtime for features you do not use

Three rules that prevent most bugs:

  1. Own resources with objects whose destructor frees them (std::vector, std::string, std::unique_ptr), never with raw new/delete.
  2. A reference, pointer, iterator, std::string_view or std::span must never outlive the object it points into.
  3. Compile with warnings and sanitizers on while developing; treat every warning as a bug.

Toolchain and build

# One file, debug build with every safety net
g++ -std=c++23 -Wall -Wextra -Wpedantic -Wconversion \
    -g -fsanitize=address,undefined main.cpp -o main
./main
 
# Optimized release build
clang++ -std=c++23 -O2 -DNDEBUG main.cpp -o main
FlagMeaning
-std=c++23language version (c++20, c++17); MSVC: /std:c++latest
-Wall -Wextra -Wpedanticthe warnings you want; add -Werror in CI
-Wconversion -Wsign-conversioncatch silent narrowing and signed/unsigned mixing
-O0 / -O2 / -O3optimization level; -O0 -g for debugging
-gdebug symbols for gdb / lldb
-fsanitize=address,undefinedAddressSanitizer + UBSan: out-of-bounds, use-after-free, overflow
-fsanitize=threadThreadSanitizer: data races (not combinable with ASan)
-D_GLIBCXX_ASSERTIONSbounds-check operator[] and friends in libstdc++
-DNDEBUGdisables assert()
-I dir, -L dir, -l nameheader search path, library search path, link a library

Real projects use CMake (generate) plus Ninja or Make (build):

CMakeLists.txt
cmake_minimum_required(VERSION 3.28)
project(app LANGUAGES CXX)
 
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)  # for clangd
 
add_executable(app src/main.cpp src/geometry.cpp)
target_compile_options(app PRIVATE -Wall -Wextra)
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
cmake --build build
./build/app
ToolJob
clangdlanguage server (reads compile_commands.json)
clang-formatformatter (.clang-format file)
clang-tidylinter with modernisation checks
gdb / lldbdebuggers
vcpkg / conanpackage managers
valgrindmemory checker (slower alternative to ASan)
Compiler Explorer (opens in a new tab)see the assembly and try any compiler online

Program structure

main.cpp
#include <print>    // std::println (C++23)
#include <string>
#include <vector>
 
namespace geo {
double area(double w, double h) { return w * h; }
}  // namespace geo
 
int main(int argc, char* argv[]) {
    std::vector<std::string> args(argv, argv + argc);
    std::println("{} args, area {}", args.size(),
                 geo::area(2.0, 3.5));
    return 0;  // optional in main; 0 means success
}

Split code into a header (declarations, shared) and a source file (definitions, compiled once):

geometry.hpp
#pragma once  // include guard
#include <string>
 
namespace geo {
struct Rect {
    double w{}, h{};
    double area() const;  // declared here
};
std::string describe(const Rect& r);
}  // namespace geo
geometry.cpp
#include "geometry.hpp"
#include <format>
 
namespace geo {
double Rect::area() const { return w * h; }
 
std::string describe(const Rect& r) {
    return std::format("{}x{}", r.w, r.h);
}
}  // namespace geo
RuleDetail
#include <x> vs "x"angle brackets for system/library headers, quotes for your own
One Definition Rulea function or variable may be defined once per program; headers hold declarations, inline functions, templates and constexpr
inline in headerslets a function or (C++17) variable be defined in a header included by many files
Unnamed namespacecontents visible only inside this .cpp (like a non-exported module member)
using namespace std;never in a header; avoid in general, it pollutes lookup
Modulesimport std; (C++23) works on recent MSVC, Clang/libc++ and GCC 15, but build-system support is still uneven; #include remains the safe default

Types and literals

TypeTypical sizeNotes
bool1 bytetrue/false; converts to and from integers implicitly
char1 bytesignedness is implementation-defined (signed on x86, unsigned on ARM Linux)
int32 bitsguaranteed at least 16; default integer
long32 (Windows) / 64 (Linux, macOS)non-portable width: prefer <cstdint>
long long64 bitsat least 64
std::int32_t, std::uint64_texactfrom <cstdint>; use when width matters
std::size_t64 bits on 64-bitunsigned; sizes and indexes in the library
std::ptrdiff_t64 bits on 64-bitsigned; pointer differences, std::ssize(v)
float, double32, 64 bitsIEEE 754 in practice; double is the default
std::stringowningmutable, contiguous chars, not Unicode-aware
std::string_view2 wordsnon-owning view of characters
LiteralTypeNote
42, 42u, 42l, 42ll, 42uzint, unsigned, long, long long, size_tuz is C++23
0x2A, 052, 0b101010inthex, octal (leading zero!), binary
1'000'000intdigit separators
3.14, 3.14f, 1e-9double, float, double
'a'charsingle quotes are characters
"hi"const char[3]a C array, not a std::string
"hi"s, "hi"svstd::string, std::string_viewneed using namespace std::literals;
R"(C:\path "x")"raw stringno escapes
nullptrstd::nullptr_tnever NULL or 0 for pointers
#include <cstdint>
#include <limits>
 
auto a = 42;             // int
auto b = 42u;            // unsigned int
auto c = 2.0f;           // float
const auto& d = a;       // const int&: auto drops & and
                         // const, so write them yourself
std::int64_t big = std::numeric_limits<std::int32_t>::max();
big += 1;                // fine in 64 bits
 
constexpr int kSize = 16;   // compile-time constant
const int n = read_int();   // runtime value, never changes
KeywordMeaning
constcannot be modified through this name
constexprvariable: known at compile time; function: may run at compile time
constevalfunction must run at compile time (C++20)
constinitstatic variable initialized at compile time, still mutable (C++20)
autodeduce the type from the initializer
decltype(expr)the declared type of an expression
using Id = std::uint64_t;type alias (prefer over typedef)
enum class Dir { up, down };scoped, strongly typed enum; std::to_underlying(d) (C++23) gives the integer

Initialization and conversions

Uninitialised locals of built-in type hold garbage, and reading them is undefined behavior. Always initialize.

int a;            // indeterminate: reading it is UB
int b{};          // 0  (value-initialized)
int c{7};         // brace init: refuses narrowing
int d = 7.9;      // compiles, d == 7 (silent truncation)
// int e{7.9};    // error: narrowing conversion
 
std::vector<int> v1(3, 1);  // {1, 1, 1}   size 3, value 1
std::vector<int> v2{3, 1};  // {3, 1}      initializer_list!
 
struct Point { int x{}, y{}; };
Point p{.x = 1, .y = 2};    // designated init (C++20)
 
Widget w();       // most vexing parse: declares a function
Widget w2{};      // an object
ConversionToolNote
numeric, up/down a class hierarchy (unchecked)static_cast<T>(x)the everyday cast
down a polymorphic hierarchy, checkeddynamic_cast<T*>(p)nullptr on failure; needs a virtual function
remove constconst_cast<T&>(x)writing to a truly-const object is UB
reinterpret bits of a pointerreinterpret_cast<T*>(p)almost always wrong outside low-level code
reinterpret bits of a valuestd::bit_cast<T>(x)C++20, safe type punning
C-style (int)xavoidtries all of the above silently
string ↔ numberstd::to_string, std::stoi, std::from_charsfrom_chars is fastest, no exceptions, no locale

Arithmetic gotchas (unlike Python):

ExpressionResultWhy
7 / 23integer division truncates
-7 / 2, -7 % 2-3, -1truncates toward zero; Python gives -4 and 1
7 / 2.03.5one double operand promotes the other
INT_MAX + 1undefined behaviorsigned overflow is UB, not wrap-around
0u - 14294967295unsigned arithmetic wraps modulo 2ⁿ
-1 < 0ufalse-1 converts to a huge unsigned; use std::cmp_less(-1, 0u)
v.size() - 1 on empty vhuge numbersize() is unsigned; use std::ssize(v) - 1
'a' + 198 (int)small types promote to int
0.1 + 0.2 == 0.3falseIEEE 754, as everywhere

Control flow

// if with an initializer (C++17): `it` is scoped to the if
if (auto it = m.find(key); it != m.end()) {
    use(it->second);
} else {
    std::println("missing {}", key);
}
 
for (int i = 0; i < n; ++i) { /* classic */ }
 
for (const auto& name : names) { /* read each */ }
for (auto& x : values) x *= 2;   // modify in place
 
for (auto [key, val] : m) { /* structured bindings */ }
 
switch (dir) {
case Dir::up:
    ++y;
    break;                 // no break → falls through
case Dir::down:
    --y;
    [[fallthrough]];       // intentional: silences warning
default:
    log();
}
 
while (running) { if (done()) break; }
do { line = read(); } while (!line.empty());
ConstructNote
for (auto x : v)copies each element; const auto& to read, auto& to modify
for (auto x : get_vector())fine: the temporary lives for the loop
for (auto x : get().member)dangling before C++23: only the outer temporary was extended
switchintegers and enums only; no strings
gotoexists; only for breaking out of nested loops, if at all
cond ? a : bas in TS
&&, ||, !return bool, not an operand (unlike Python's and/or)
and, or, notvalid alternative spellings

Functions and lambdas

int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }  // overload
 
void greet(std::string_view name, bool loud = false);
 
auto square(auto x) { return x * x; }   // generic (C++20)
 
[[nodiscard]] int parse(std::string_view s);  // must use

How to pass parameters:

ParameterUse forCaller sees changes?
T (by value)cheap types (int, double, views, small structs) and "sink" arguments you will storeno
const T&read-only access to anything expensive to copyno
T&in-out: the function modifies the caller's objectyes
T&&take ownership of a temporary (move constructors, sinks)object is moved from
std::string_view, std::span<const T>read-only strings and arrays of any originno
T*optional in-out (may be nullptr)yes

Return by value. Copies of returned locals are elided or moved automatically; never write return std::move(local);, it blocks the optimization.

std::vector<int> evens(int n) {
    std::vector<int> out;
    for (int i = 0; i < n; i += 2) out.push_back(i);
    return out;  // no copy
}
 
struct MinMax { int lo, hi; };
MinMax bounds(std::span<const int> xs);
auto [lo, hi] = bounds(v);  // several results: return a
                            // struct, unpack with bindings

Lambdas are anonymous function objects. The capture list says what they copy or reference:

int base = 10;
auto add_base = [base](int x) { return x + base; };  // copy
auto bump = [&base] { ++base; };                      // ref
auto by_val = [=] { return base; };  // copy all used
auto by_ref = [&] { return base; };  // reference all used
 
auto counter = [n = 0]() mutable { return ++n; };
auto owner = [p = std::move(ptr)] { return *p; };  // move in
 
std::ranges::sort(people, [](const auto& a, const auto& b) {
    return a.age < b.age;
});
 
std::function<int(int)> f = add_base;  // type-erased holder
CaptureMeaning
[]nothing
[x], [&x]x by copy, by reference
[=], [&]everything used, by copy / by reference
[this], [*this]the object by pointer / by copy
[y = expr]new member initialized from expr (use for moves)
mutablecopied captures may be modified

A lambda capturing by reference must not outlive what it references: returning [&] lambdas or storing them in callbacks is a classic dangling bug.

References, pointers and memory

Reference T&Pointer T*
Null?nevermay be nullptr
Rebind?no, bound once at creationyes
Syntaxused like the object*p to dereference, p->m for members, &x to take an address
Use forparameters, aliasesoptional or re-seatable non-owning links, C APIs
int x = 1;
int& r = x;     // r is another name for x
r = 5;          // x == 5
 
int* p = &x;    // p holds x's address
*p = 7;         // x == 7
p = nullptr;    // p now points nowhere
 
const int* pc = &x;        // pointer to const int
int* const cp = &x;        // const pointer to int
const int* const both = &x;

Where objects live:

StorageCreatedDestroyedExample
automatic (stack)at declarationat end of scope, in reverse orderstd::string s;
dynamic (heap)new / make_uniquedelete / owner's destructorstd::make_unique<Node>()
staticbefore main (or first use for function-local static)after mainglobals, static int count;
threadper threadthread exitthread_local int id;

A std::vector<T> object is small and lives on the stack; its elements live on the heap and are freed by its destructor. That is RAII: the stack object owns the heap memory.

Dangling, the most common memory bug:

int& bad_ref() { int local = 1; return local; }  // dangles
 
std::string_view bad_view() {
    std::string s = "temp";
    return s;              // view of a destroyed string
}
 
std::vector<int> v{1, 2, 3};
int& first = v[0];
v.push_back(4);            // may reallocate: `first` dangles

RAII and smart pointers

Resource Acquisition Is Initialization: acquire a resource in a constructor, release it in the destructor. Destructors run on every exit from a scope, including return and exceptions, so cleanup cannot be forgotten. It is Python's with and TypeScript's using, applied to everything automatically.

#include <fstream>
#include <memory>
#include <mutex>
 
void save(const std::string& path, std::string_view text) {
    std::ofstream out(path);   // opens
    out << text;
}                              // closes, even on exception
 
std::mutex m;
void critical() {
    std::scoped_lock lock(m);  // locks
    // ...
}                              // unlocks
Smart pointerOwnershipCopy?Create with
std::unique_ptr<T>exactly one owner; zero overhead over a raw pointerno, move onlystd::make_unique<T>(args...)
std::shared_ptr<T>shared, reference-counted (atomic count)yesstd::make_shared<T>(args...)
std::weak_ptr<T>non-owning observer of a shared_ptryesfrom a shared_ptr; .lock() to use
T*, T&non-owningyes.get(), *ptr
struct Node {
    int value{};
    std::unique_ptr<Node> next;  // owns the rest of the list
};
 
auto head = std::make_unique<Node>(1);
head->next = std::make_unique<Node>(2);
auto moved = std::move(head);    // head is now nullptr
 
auto cfg = std::make_shared<Config>();
std::weak_ptr<Config> watch = cfg;
if (auto c = watch.lock()) use(*c);  // still alive?

Default to unique_ptr; reach for shared_ptr only when lifetime is genuinely shared. Two shared_ptrs pointing at each other leak: break cycles with weak_ptr. Pass T& or T* to functions that only use the object; pass the smart pointer only when the function takes or shares ownership.

Classes

struct and class are the same except for default access (public vs private). Convention: struct for plain data, class when there are invariants.

class Account {
public:
    explicit Account(std::string owner, long cents = 0)
        : owner_{std::move(owner)}, cents_{cents} {}
 
    void deposit(long c) {
        if (c <= 0) throw std::invalid_argument("amount");
        cents_ += c;
    }
    long balance() const { return cents_; }  // read-only
    const std::string& owner() const { return owner_; }
 
    bool operator==(const Account&) const = default;
    auto operator<=>(const Account&) const = default;
 
private:
    std::string owner_;
    long cents_{};
    static inline int count_ = 0;  // one per class
};
 
Account a{"ada", 500};
// Account b = "bob";  // error: constructor is explicit
FeatureNote
Member initializer list : a_{x}initializes members; assignment in the body would construct then overwrite
Initialization ordermembers are initialized in declaration order, whatever the list says
explicitblocks implicit conversions; put it on single-argument constructors
const member functioncallable on const objects; cannot change members (except mutable ones)
= default / = deleteask for the compiler version / forbid (e.g. copying)
operator<=> = defaultgenerates <, <=, >, >= and a defaulted ==, comparing members in order
static membershared by all instances; static inline defines it in the header
friendgrants a function or class access to private members

Inheritance and runtime polymorphism:

class Shape {
public:
    virtual ~Shape() = default;       // required: see below
    virtual double area() const = 0;  // pure virtual
};
 
class Circle final : public Shape {
public:
    explicit Circle(double r) : r_{r} {}
    double area() const override {
        return 3.14159 * r_ * r_;
    }
private:
    double r_;
};
 
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(2.0));
for (const auto& s : shapes) std::println("{}", s->area());
KeywordMeaning
virtualdispatched at runtime through a vtable
= 0pure virtual: the class is abstract
overridecompile error if it does not override a base virtual; always write it
finalcannot be overridden further / derived from
virtual destructora base class deleted through a base pointer must have one, or it is UB
slicingcopying a Circle into a Shape by value keeps only the Shape part; hold polymorphic objects by pointer or reference

Prefer composition, and std::variant (below) for a closed set of alternatives; inheritance fits open sets of types behind one interface.

Copy, move and the rule of zero

Special memberSignatureGenerated when
default constructorT()you declare no constructors
copy constructorT(const T&)not declared, and no move operation declared
copy assignmentT& operator=(const T&)same
move constructorT(T&&) noexceptno copy, move or destructor declared
move assignmentT& operator=(T&&) noexceptsame
destructor~T()always, unless declared
  • Rule of zero: build classes from members that manage themselves (std::string, std::vector, std::unique_ptr) and declare none of the five. This is the goal.
  • Rule of five: if you must write one of destructor, copy constructor, copy assignment, move constructor, move assignment, write (or = default/= delete) all five.
  • Mark move operations noexcept: std::vector only moves elements during reallocation if moving cannot throw; otherwise it copies.

Moving transfers the guts (heap pointer, size) from an object that is about to die or that you no longer need, leaving it valid but unspecified (for library types, usually empty). std::move does not move anything: it is a cast to T&& that allows a move.

std::string a = "a long string that is heap allocated";
std::string b = a;             // copy: a is untouched
std::string c = std::move(a);  // move: a is now empty-ish
a = "reuse";                   // OK: assign before reuse
 
std::vector<std::string> names;
std::string n = read_name();
names.push_back(std::move(n));  // no copy of the buffer
names.emplace_back("built in place");
 
auto p = std::make_unique<int>(1);
// auto q = p;              // error: unique_ptr can't copy
auto q = std::move(p);      // ownership transferred
Value categoryWhat it isBinds to
lvaluehas a name / an address (x, v[0], *p)T&, const T&
prvaluea temporary (42, f(), T{})T&&, const T&
xvaluean lvalue cast for moving (std::move(x))T&&, const T&

Templates and concepts

Templates are compile-time code generation: the compiler stamps out a copy per set of arguments. Definitions must be visible where used, so templates live in headers.

#include <concepts>
 
template <typename T>
T max_of(T a, T b) { return a < b ? b : a; }
 
max_of(3, 7);          // T = int
max_of<double>(3, 7.5);
 
template <typename T, std::size_t N>
struct FixedStack {
    std::array<T, N> items{};
    std::size_t size = 0;
    void push(const T& x) { items.at(size++) = x; }
};
FixedStack<int, 8> s;
 
// Concepts (C++20) constrain T and give readable errors
template <std::integral T>
T gcd(T a, T b) { return b == 0 ? a : gcd(b, a % b); }
 
template <typename T>
concept Shape = requires(const T& s) {
    { s.area() } -> std::convertible_to<double>;
};
 
template <std::ranges::range R>
    requires Shape<std::ranges::range_value_t<R>>
double total(const R& shapes);
 
void log_all(const auto&... args) {   // variadic
    (std::print("{} ", args), ...);    // fold expression
}
Concept (<concepts>)Satisfied by
std::integral, std::floating_pointbuilt-in integer / floating types
std::same_as<T, U>, std::convertible_to<T, U>type relations
std::equality_comparable, std::totally_ordered== / all comparisons
std::invocable<F, Args...>callable with those arguments
std::ranges::range, std::ranges::random_access_rangeiterable things

if constexpr (cond) discards the untaken branch at compile time, which lets one template handle different types. Compile-time duck typing with concepts is the C++ equivalent of TypeScript structural types.

Standard library essentials

NeedUseTS / Python analogue
growable arraystd::vector<T>Array, list
fixed arraystd::array<T, N>tuple of one type
textstd::string, std::string_viewstring, str
ordered map / setstd::map, std::set (red-black tree, O(log n))sorted containers
hash map / setstd::unordered_map, std::unordered_set (O(1) average)Map, dict, Set, set
double-ended queuestd::deque<T>collections.deque
priority queuestd::priority_queue<T> (max-heap)heapq (min-heap)
maybe a valuestd::optional<T>T | undefined, Optional[T]
one of several typesstd::variant<A, B> + std::visittagged union
value or errorstd::expected<T, E> (C++23)Result types
view of contiguous datastd::span<T>slice without copy
pair / tuplestd::pair, std::tupletuples
formattingstd::format, std::print, std::printlntemplate literals, f-strings
#include <map>
#include <optional>
#include <print>
#include <unordered_map>
#include <vector>
 
std::vector<int> v{5, 3, 8};
v.push_back(1);
v.size();          // 4 (std::size_t)
v[0];              // unchecked: out of range is UB
v.at(10);          // throws std::out_of_range
v.reserve(100);    // pre-allocate capacity
std::erase_if(v, [](int x) { return x > 4; });  // C++20
 
std::unordered_map<std::string, int> ages{{"ada", 36}};
ages["bob"] = 41;          // [] inserts if missing!
if (ages.contains("ada")) { /* C++20 */ }
if (auto it = ages.find("eve"); it != ages.end())
    std::println("{}", it->second);
 
std::map<std::string, int> sorted(ages.begin(), ages.end());
for (const auto& [name, age] : sorted)
    std::println("{:<6}{:>4}", name, age);
 
std::optional<int> find_id(std::string_view name);
if (auto id = find_id("ada")) std::println("{}", *id);
int id = find_id("x").value_or(-1);

Algorithms and ranges (C++20/23) replace hand-written loops:

#include <algorithm>
#include <numeric>
#include <ranges>
 
namespace rv = std::views;
 
std::ranges::sort(v);                    // ascending
std::ranges::sort(v, std::greater{});    // descending
auto it = std::ranges::find(v, 8);
bool neg = std::ranges::any_of(v, [](int x) {
    return x < 0;
});
int sum = std::accumulate(v.begin(), v.end(), 0);
auto [lo, hi] = std::ranges::minmax(v);
 
auto squares = v
    | rv::filter([](int x) { return x % 2 == 0; })
    | rv::transform([](int x) { return x * x; })
    | std::ranges::to<std::vector>();    // C++23
 
for (auto [i, x] : rv::enumerate(v))     // C++23
    std::println("{}: {}", i, x);
for (int i : rv::iota(0, 5)) { /* 0..4 */ }
Container gotchaDetail
iterator invalidationpush_back, insert or erase on a vector can invalidate every pointer, reference and iterator into it
map[key]inserts a default value when the key is missing; use find, contains or at to read
std::vector<bool>packed bits, not a real container of bool; &v[0] does not give a bool*
std::string_view / std::spannever store one that points into a temporary
std::string from char*constructing from a null char* is UB (a literal nullptr is a compile error since C++23)

Errors and exceptions

#include <expected>
#include <stdexcept>
 
double safe_div(double a, double b) {
    if (b == 0) throw std::domain_error("divide by zero");
    return a / b;
}
 
try {
    safe_div(1, 0);
} catch (const std::domain_error& e) {   // catch by const&
    std::println("math: {}", e.what());
} catch (const std::exception& e) {      // base class
    std::println("other: {}", e.what());
}
 
enum class ParseErr { empty, bad_digit };
 
std::expected<int, ParseErr> to_int(std::string_view s) {
    if (s.empty()) return std::unexpected(ParseErr::empty);
    int n = 0;
    for (char c : s) {
        if (c < '0' || c > '9')
            return std::unexpected(ParseErr::bad_digit);
        n = n * 10 + (c - '0');  // (no overflow check)
    }
    return n;
}
 
if (auto r = to_int("42")) std::println("{}", *r);
else if (r.error() == ParseErr::empty) { /* ... */ }
ToolUse for
exceptionsrare, truly exceptional failures that callers far up the stack handle; constructors that cannot establish their invariant
std::expected<T, E>expected, recoverable failures (parsing, I/O, validation) handled by the direct caller
std::optional<T>"no result" without a reason
assert(cond)programmer errors; removed by -DNDEBUG
noexceptpromise not to throw; if it does, std::terminate is called. Destructors are implicitly noexcept
std::terminate / std::abortunrecoverable: no unwinding

Throw by value, catch by const&. Exceptions cost almost nothing on the happy path and a lot when thrown, so never use them for ordinary control flow. Code is exception-safe when every resource is owned by an RAII object.

Undefined behavior and pitfalls

Undefined behavior (UB) means the standard places no requirements on the program. The optimizer assumes UB never happens, so the result is not "a crash" but anything: wrong answers, deleted checks, security holes, code that works in debug and fails in release.

UBExampleCatch it with
out-of-bounds accessv[v.size()], arr[-1]ASan, .at(), _GLIBCXX_ASSERTIONS
use after free / danglingreturned reference to a local, view into a temporaryASan, Clang -Wdangling, GCC -Wdangling-reference
signed integer overflowINT_MAX + 1UBSan, -ftrapv, wider types
null pointer dereference*p when p == nullptrUBSan, check first
reading uninitialised memoryint x; f(x);-Wall, MemorySanitizer, always initialize
data racetwo threads, one writing, no synchronizationTSan, std::mutex, std::atomic
shift by width or more1 << 32 for a 32-bit intUBSan
strict aliasing violationreading a float through an int*std::bit_cast, std::memcpy
modifying a string literalchar* s = (char*)"hi"; s[0] = 'H';never cast away const
missing return in a non-void functionfalls off the end-Wreturn-type (on in -Wall)

Other traps that are legal but wrong:

TrapFix
if (x = 5)-Wall warns; write comparisons as intended
order of evaluation of function arguments is unspecifieddo not depend on it: f(next(), next())
implicit conversions via single-argument constructorsexplicit
header-only changes forcing huge rebuildsforward declarations, fewer includes in headers
using namespace std; in a headerqualify names
object slicingpass polymorphic objects by reference or pointer
forgetting a virtual destructorvirtual ~Base() = default;
auto hiding a copy: auto x = v[0]; for a big typeconst auto&
static initialization order across .cpp filesfunction-local static or constinit

Threads, briefly:

#include <atomic>
#include <mutex>
#include <thread>
 
std::atomic<int> hits{0};
std::mutex m;
std::vector<int> shared;
 
{
    std::jthread t1([&] { hits++; });  // joins on scope exit
    std::jthread t2([&] {
        std::scoped_lock lock(m);
        shared.push_back(1);
    });
}  // both threads joined here

Recipes

Read a file into lines:

#include <fstream>
#include <stdexcept>
#include <string>
#include <vector>
 
std::vector<std::string> read_lines(const std::string& p) {
    std::ifstream in(p);
    if (!in) throw std::runtime_error("cannot open " + p);
    std::vector<std::string> lines;
    for (std::string line; std::getline(in, line);)
        lines.push_back(std::move(line));
    return lines;
}

Split a string without copying:

#include <ranges>
#include <string_view>
 
std::vector<std::string_view> split(std::string_view s,
                                    char sep) {
    std::vector<std::string_view> out;
    for (auto part : s | std::views::split(sep))
        out.emplace_back(part.begin(), part.end());
    return out;  // views into `s`: keep `s` alive
}

Parse a number safely:

#include <charconv>
#include <optional>
 
std::optional<int> parse_int(std::string_view s) {
    int value{};
    const char* end = s.data() + s.size();
    auto [ptr, ec] = std::from_chars(s.data(), end, value);
    if (ec != std::errc{} || ptr != end)
        return std::nullopt;
    return value;
}

Count words with a hash map, then print the top five:

std::unordered_map<std::string, int> counts;
for (std::string w; std::cin >> w;) ++counts[w];
 
std::vector<std::pair<std::string, int>> top(
    counts.begin(), counts.end());
auto k = std::min<std::size_t>(5, top.size());
std::ranges::partial_sort(top, top.begin() + k,
    [](const auto& a, const auto& b) {
        return a.second > b.second;
    });
for (auto& [w, n] : top | std::views::take(k))
    std::println("{:>5} {}", n, w);

Visit a std::variant with overloaded lambdas:

#include <variant>
 
template <class... Fs> struct overloaded : Fs... {
    using Fs::operator()...;
};
 
using Json = std::variant<std::nullptr_t, bool, double,
                          std::string>;
 
std::string show(const Json& j) {
    return std::visit(overloaded{
        [](std::nullptr_t) { return std::string{"null"}; },
        [](bool b) -> std::string {
            return b ? "true" : "false";
        },
        [](double d) { return std::format("{}", d); },
        [](const std::string& s) { return '"' + s + '"'; },
    }, j);
}

Time a block of code:

#include <chrono>
 
auto t0 = std::chrono::steady_clock::now();
run_work();
auto dt = std::chrono::steady_clock::now() - t0;
std::println("{}", std::chrono::duration_cast<
    std::chrono::milliseconds>(dt));   // e.g. "12ms"

Random numbers (never rand()):

#include <random>
 
std::mt19937 rng{std::random_device{}()};
std::uniform_int_distribution<int> die{1, 6};
std::normal_distribution<double> noise{0.0, 1.0};
int roll = die(rng);
double eps = noise(rng);

A minimal RAII wrapper for a C handle:

#include <cstdio>
#include <memory>
 
struct FileCloser {
    void operator()(std::FILE* f) const { std::fclose(f); }
};
using File = std::unique_ptr<std::FILE, FileCloser>;
 
File open_file(const char* path, const char* mode) {
    return File{std::fopen(path, mode)};  // null on error
}

References