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.
| Idea | TypeScript / Python | C++ |
|---|---|---|
| Variable | name bound to a heap object | the object itself, usually on the stack |
b = a | both names share one object | b is an independent copy (unless a is a reference or pointer) |
| Memory | garbage collector | scope-based: destructors run at } (RAII) |
| Types | checked (TS) or ignored (Py) at runtime | fixed at compile time; no runtime type info by default |
| Errors | exceptions, runtime checks everywhere | exceptions or std::expected; most misuse is undefined behavior, not an error |
| Generics | erased (TS) / duck typing (Py) | templates: a separate compiled copy per type |
| Build | interpreter / bundler | preprocess → compile each .cpp → link into a native binary |
| "Zero-cost" | no | you do not pay at runtime for features you do not use |
Three rules that prevent most bugs:
- Own resources with objects whose destructor frees them (
std::vector,std::string,std::unique_ptr), never with rawnew/delete. - A reference, pointer, iterator,
std::string_vieworstd::spanmust never outlive the object it points into. - 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| Flag | Meaning |
|---|---|
-std=c++23 | language version (c++20, c++17); MSVC: /std:c++latest |
-Wall -Wextra -Wpedantic | the warnings you want; add -Werror in CI |
-Wconversion -Wsign-conversion | catch silent narrowing and signed/unsigned mixing |
-O0 / -O2 / -O3 | optimization level; -O0 -g for debugging |
-g | debug symbols for gdb / lldb |
-fsanitize=address,undefined | AddressSanitizer + UBSan: out-of-bounds, use-after-free, overflow |
-fsanitize=thread | ThreadSanitizer: data races (not combinable with ASan) |
-D_GLIBCXX_ASSERTIONS | bounds-check operator[] and friends in libstdc++ |
-DNDEBUG | disables assert() |
-I dir, -L dir, -l name | header search path, library search path, link a library |
Real projects use CMake (generate) plus Ninja or Make (build):
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| Tool | Job |
|---|---|
clangd | language server (reads compile_commands.json) |
clang-format | formatter (.clang-format file) |
clang-tidy | linter with modernisation checks |
gdb / lldb | debuggers |
vcpkg / conan | package managers |
valgrind | memory checker (slower alternative to ASan) |
| Compiler Explorer (opens in a new tab) | see the assembly and try any compiler online |
Program structure
#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):
#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#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| Rule | Detail |
|---|---|
#include <x> vs "x" | angle brackets for system/library headers, quotes for your own |
| One Definition Rule | a function or variable may be defined once per program; headers hold declarations, inline functions, templates and constexpr |
inline in headers | lets a function or (C++17) variable be defined in a header included by many files |
Unnamed namespace | contents visible only inside this .cpp (like a non-exported module member) |
using namespace std; | never in a header; avoid in general, it pollutes lookup |
| Modules | import 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
| Type | Typical size | Notes |
|---|---|---|
bool | 1 byte | true/false; converts to and from integers implicitly |
char | 1 byte | signedness is implementation-defined (signed on x86, unsigned on ARM Linux) |
int | 32 bits | guaranteed at least 16; default integer |
long | 32 (Windows) / 64 (Linux, macOS) | non-portable width: prefer <cstdint> |
long long | 64 bits | at least 64 |
std::int32_t, std::uint64_t | exact | from <cstdint>; use when width matters |
std::size_t | 64 bits on 64-bit | unsigned; sizes and indexes in the library |
std::ptrdiff_t | 64 bits on 64-bit | signed; pointer differences, std::ssize(v) |
float, double | 32, 64 bits | IEEE 754 in practice; double is the default |
std::string | owning | mutable, contiguous chars, not Unicode-aware |
std::string_view | 2 words | non-owning view of characters |
| Literal | Type | Note |
|---|---|---|
42, 42u, 42l, 42ll, 42uz | int, unsigned, long, long long, size_t | uz is C++23 |
0x2A, 052, 0b101010 | int | hex, octal (leading zero!), binary |
1'000'000 | int | digit separators |
3.14, 3.14f, 1e-9 | double, float, double | |
'a' | char | single quotes are characters |
"hi" | const char[3] | a C array, not a std::string |
"hi"s, "hi"sv | std::string, std::string_view | need using namespace std::literals; |
R"(C:\path "x")" | raw string | no escapes |
nullptr | std::nullptr_t | never 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| Keyword | Meaning |
|---|---|
const | cannot be modified through this name |
constexpr | variable: known at compile time; function: may run at compile time |
consteval | function must run at compile time (C++20) |
constinit | static variable initialized at compile time, still mutable (C++20) |
auto | deduce 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| Conversion | Tool | Note |
|---|---|---|
| numeric, up/down a class hierarchy (unchecked) | static_cast<T>(x) | the everyday cast |
| down a polymorphic hierarchy, checked | dynamic_cast<T*>(p) | nullptr on failure; needs a virtual function |
remove const | const_cast<T&>(x) | writing to a truly-const object is UB |
| reinterpret bits of a pointer | reinterpret_cast<T*>(p) | almost always wrong outside low-level code |
| reinterpret bits of a value | std::bit_cast<T>(x) | C++20, safe type punning |
C-style (int)x | avoid | tries all of the above silently |
| string ↔ number | std::to_string, std::stoi, std::from_chars | from_chars is fastest, no exceptions, no locale |
Arithmetic gotchas (unlike Python):
| Expression | Result | Why |
|---|---|---|
7 / 2 | 3 | integer division truncates |
-7 / 2, -7 % 2 | -3, -1 | truncates toward zero; Python gives -4 and 1 |
7 / 2.0 | 3.5 | one double operand promotes the other |
INT_MAX + 1 | undefined behavior | signed overflow is UB, not wrap-around |
0u - 1 | 4294967295 | unsigned arithmetic wraps modulo 2ⁿ |
-1 < 0u | false | -1 converts to a huge unsigned; use std::cmp_less(-1, 0u) |
v.size() - 1 on empty v | huge number | size() is unsigned; use std::ssize(v) - 1 |
'a' + 1 | 98 (int) | small types promote to int |
0.1 + 0.2 == 0.3 | false | IEEE 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());| Construct | Note |
|---|---|
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 |
switch | integers and enums only; no strings |
goto | exists; only for breaking out of nested loops, if at all |
cond ? a : b | as in TS |
&&, ||, ! | return bool, not an operand (unlike Python's and/or) |
and, or, not | valid 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 useHow to pass parameters:
| Parameter | Use for | Caller sees changes? |
|---|---|---|
T (by value) | cheap types (int, double, views, small structs) and "sink" arguments you will store | no |
const T& | read-only access to anything expensive to copy | no |
T& | in-out: the function modifies the caller's object | yes |
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 origin | no |
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 bindingsLambdas 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| Capture | Meaning |
|---|---|
[] | 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) |
mutable | copied 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? | never | may be nullptr |
| Rebind? | no, bound once at creation | yes |
| Syntax | used like the object | *p to dereference, p->m for members, &x to take an address |
| Use for | parameters, aliases | optional 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:
| Storage | Created | Destroyed | Example |
|---|---|---|---|
| automatic (stack) | at declaration | at end of scope, in reverse order | std::string s; |
| dynamic (heap) | new / make_unique | delete / owner's destructor | std::make_unique<Node>() |
| static | before main (or first use for function-local static) | after main | globals, static int count; |
| thread | per thread | thread exit | thread_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` danglesRAII 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 pointer | Ownership | Copy? | Create with |
|---|---|---|---|
std::unique_ptr<T> | exactly one owner; zero overhead over a raw pointer | no, move only | std::make_unique<T>(args...) |
std::shared_ptr<T> | shared, reference-counted (atomic count) | yes | std::make_shared<T>(args...) |
std::weak_ptr<T> | non-owning observer of a shared_ptr | yes | from a shared_ptr; .lock() to use |
T*, T& | non-owning | yes | .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| Feature | Note |
|---|---|
Member initializer list : a_{x} | initializes members; assignment in the body would construct then overwrite |
| Initialization order | members are initialized in declaration order, whatever the list says |
explicit | blocks implicit conversions; put it on single-argument constructors |
const member function | callable on const objects; cannot change members (except mutable ones) |
= default / = delete | ask for the compiler version / forbid (e.g. copying) |
operator<=> = default | generates <, <=, >, >= and a defaulted ==, comparing members in order |
static member | shared by all instances; static inline defines it in the header |
friend | grants 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());| Keyword | Meaning |
|---|---|
virtual | dispatched at runtime through a vtable |
= 0 | pure virtual: the class is abstract |
override | compile error if it does not override a base virtual; always write it |
final | cannot be overridden further / derived from |
| virtual destructor | a base class deleted through a base pointer must have one, or it is UB |
| slicing | copying 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 member | Signature | Generated when |
|---|---|---|
| default constructor | T() | you declare no constructors |
| copy constructor | T(const T&) | not declared, and no move operation declared |
| copy assignment | T& operator=(const T&) | same |
| move constructor | T(T&&) noexcept | no copy, move or destructor declared |
| move assignment | T& operator=(T&&) noexcept | same |
| 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::vectoronly 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 category | What it is | Binds to |
|---|---|---|
| lvalue | has a name / an address (x, v[0], *p) | T&, const T& |
| prvalue | a temporary (42, f(), T{}) | T&&, const T& |
| xvalue | an 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_point | built-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_range | iterable 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
| Need | Use | TS / Python analogue |
|---|---|---|
| growable array | std::vector<T> | Array, list |
| fixed array | std::array<T, N> | tuple of one type |
| text | std::string, std::string_view | string, str |
| ordered map / set | std::map, std::set (red-black tree, O(log n)) | sorted containers |
| hash map / set | std::unordered_map, std::unordered_set (O(1) average) | Map, dict, Set, set |
| double-ended queue | std::deque<T> | collections.deque |
| priority queue | std::priority_queue<T> (max-heap) | heapq (min-heap) |
| maybe a value | std::optional<T> | T | undefined, Optional[T] |
| one of several types | std::variant<A, B> + std::visit | tagged union |
| value or error | std::expected<T, E> (C++23) | Result types |
| view of contiguous data | std::span<T> | slice without copy |
| pair / tuple | std::pair, std::tuple | tuples |
| formatting | std::format, std::print, std::println | template 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 gotcha | Detail |
|---|---|
| iterator invalidation | push_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::span | never 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) { /* ... */ }| Tool | Use for |
|---|---|
| exceptions | rare, 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 |
noexcept | promise not to throw; if it does, std::terminate is called. Destructors are implicitly noexcept |
std::terminate / std::abort | unrecoverable: 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.
| UB | Example | Catch it with |
|---|---|---|
| out-of-bounds access | v[v.size()], arr[-1] | ASan, .at(), _GLIBCXX_ASSERTIONS |
| use after free / dangling | returned reference to a local, view into a temporary | ASan, Clang -Wdangling, GCC -Wdangling-reference |
| signed integer overflow | INT_MAX + 1 | UBSan, -ftrapv, wider types |
| null pointer dereference | *p when p == nullptr | UBSan, check first |
| reading uninitialised memory | int x; f(x); | -Wall, MemorySanitizer, always initialize |
| data race | two threads, one writing, no synchronization | TSan, std::mutex, std::atomic |
| shift by width or more | 1 << 32 for a 32-bit int | UBSan |
| strict aliasing violation | reading a float through an int* | std::bit_cast, std::memcpy |
| modifying a string literal | char* s = (char*)"hi"; s[0] = 'H'; | never cast away const |
missing return in a non-void function | falls off the end | -Wreturn-type (on in -Wall) |
Other traps that are legal but wrong:
| Trap | Fix |
|---|---|
if (x = 5) | -Wall warns; write comparisons as intended |
| order of evaluation of function arguments is unspecified | do not depend on it: f(next(), next()) |
| implicit conversions via single-argument constructors | explicit |
| header-only changes forcing huge rebuilds | forward declarations, fewer includes in headers |
using namespace std; in a header | qualify names |
| object slicing | pass polymorphic objects by reference or pointer |
| forgetting a virtual destructor | virtual ~Base() = default; |
auto hiding a copy: auto x = v[0]; for a big type | const auto& |
static initialization order across .cpp files | function-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 hereRecipes
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
- cppreference.com (opens in a new tab): the de facto reference for the language and standard library, with per-version support tables
- C++ Core Guidelines (opens in a new tab): Stroustrup and Sutter's rules for modern, safe C++ (source of the parameter-passing and ownership advice)
- learncpp.com (opens in a new tab): free, thorough tutorial from first principles
- isocpp.org: Get started (opens in a new tab): the Standard C++ Foundation's reading list and FAQ
- Compiler support for C++23 (opens in a new tab): which compiler version implements which feature
- GCC: Options to request or suppress warnings (opens in a new tab): what
-Wall,-Wextraand friends enable - Clang: AddressSanitizer (opens in a new tab) and UndefinedBehaviorSanitizer (opens in a new tab): runtime bug detectors
- CMake tutorial (opens in a new tab): the official step-by-step guide
- Compiler Explorer (opens in a new tab): run and inspect code on any compiler
- Bjarne Stroustrup, A Tour of C++, 3rd ed. (Addison-Wesley, 2022): the language's author on modern C++ in 250 pages