Compiled and systems languages cheat sheetC++ logo

C++ cheat sheet

C++ syntax on one page, grouped by what you are trying to do: classes, smart pointers, templates, STL containers, lambdas and move semantics, on C++23.

Last updated

C++ gives you C's speed and control, plus classes, templates and a standard library that manages memory for you if you let it. The reference below is grouped by what you are trying to do, and the filter box searches all of it at once. Type unique_ptr and every smart pointer row comes to you, or type C++20 to see what that standard added.

Every snippet is checked against C++23 (ISO/IEC 14882:2024) with GCC 15. GCC still compiles as C++17 by default, so pass -std=c++23. Anything newer than C++17 says so in the notes column, so you can tell at a glance whether it will build with the compiler you have. Names like v, m and Widget are placeholders for your own. If you do not have GCC installed, the official Docker image is the quickest way to try something: docker run --rm -it -v "$PWD":/src -w /src gcc:15 bash, and the Docker cheat sheet has the rest. Coming from C? The C cheat sheet covers the pointers, arrays and printf formats that C++ inherits.

Compiling and building

TaskCommandNotes
Check which version you haveg++ --version
Compile one file into a programg++ -std=c++23 main.cpp -o mainUse g++, not gcc, so the C++ standard library is linked
Run it./main
Pick a C++ standardg++ -std=c++20 main.cpp -o mainOr c++17, c++23. GCC 15 defaults to gnu++17, so always set it
Turn on the warnings worth havingg++ -std=c++23 -Wall -Wextra -Wpedantic main.cpp -o main
Treat every warning as an errorg++ -std=c++23 -Wall -Wextra -Werror main.cpp -o main
Build for a debuggerg++ -std=c++23 -g -O0 main.cpp -o main
Build optimisedg++ -std=c++23 -O2 main.cpp -o main-O3 is more aggressive, -Os favours size
Catch memory bugs as they happeng++ -std=c++23 -g -fsanitize=address,undefined main.cpp -o mainOut-of-bounds reads, use after free, overflow
Bounds-check operator[] and iteratorsg++ -std=c++23 -D_GLIBCXX_ASSERTIONS main.cpp -o mainlibstdc++ only. Cheap enough to leave on in debug builds
Compile several files into one programg++ -std=c++23 main.cpp shape.cpp -o app
Compile only, without linkingg++ -std=c++23 -c shape.cppWrites shape.o
Link object filesg++ main.o shape.o -o app
Look for headers in another folderg++ -std=c++23 -I include main.cpp -o main
Link a library from another folderg++ main.cpp -L lib -lfoo -o mainFinds lib/libfoo.so or lib/libfoo.a
Use clang insteadclang++ -std=c++23 main.cpp -o mainTakes the same flags
Configure a CMake projectcmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
Build a CMake projectcmake --build build
Step through it in a debuggergdb ./mainbreak main, run, next, print x

On macOS, g++ is Apple's clang++ under another name unless you install GCC from Homebrew. Every flag on this page works with clang++ as well, apart from -D_GLIBCXX_ASSERTIONS, which is specific to GCC's standard library.

Types and variables

TaskCodeNotes
Integerint n = 42;4 bytes on every common platform
Wider integerlong long big = 9'000'000'000LL;8 bytes. The ' is a digit separator
Floating pointdouble d = 3.14; float f = 3.14f;double is the default for decimals
Booleanbool done = false;true and false are keywords
Characterchar c = 'A';1 byte
Exact-width integerstd::int32_t id = 7; std::uint64_t mask = 0;#include <cstdint>
Size or countstd::size_t len = v.size();Unsigned. What .size() returns for the standard containers
Brace initialisationint n{42};Refuses narrowing: int n{3.7} does not compile
Let the compiler infer the typeauto ratio = 1.5;double. Needs an initialiser
Same type as another expressiondecltype(x) y = x;
Read-only variableconst double rate = 0.2;
Compile-time constantconstexpr int max_users = 100;Usable as an array size or template argument
Null pointerint* p = nullptr;Not NULL or 0
Scoped enumenum class Color { Red, Green, Blue }; Color c = Color::Red;No silent conversion to int, unlike plain enum
Give a type another nameusing Id = std::uint64_t;The modern typedef
Convert to another typestatic_cast<double>(total) / countCast one side, or the division is whole-number
Hex, octal and binary literals0xFF 0755 0b1010A leading 0 means octal, so 010 is 8
Largest and smallest intstd::numeric_limits<int>::max()#include <limits>. Also ::min() and ::lowest()
Maths constantsstd::numbers::piC++20. #include <numbers>
size_t literalauto i = 0uz;C++23

Strings and I/O

TaskCodeNotes
Make a stringstd::string name = "Ada";#include <string>. Owns its characters and grows as needed
Lengthname.size()Also name.length(). empty() checks for zero
Join stringsstd::string full = first + " " + last;At least one side of each + must be a std::string
Appendname += "!"; name.push_back('?');
Compareif (answer == "yes") { }Compares text, unlike two C char pointers
One charactername[0] name.at(0)at() throws std::out_of_range, [] does not check
Substrings.substr(0, 3)Start and length, not start and end
Find textauto pos = s.find("cat");std::string::npos if it is not there
Containss.contains("cat")C++23
Starts or ends withpath.starts_with("/") file.ends_with(".cpp")C++20
Loop over the charactersfor (char c : s) { }
Number to textstd::to_string(42)
Text to a numberint n = std::stoi(s); double d = std::stod(s);Throws std::invalid_argument on bad input
Text to a number, no exceptionsauto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), n);#include <charconv>. Check ec == std::errc{}
Read-only view, no copyvoid greet(std::string_view name);#include <string_view>. Never let it outlive the string
Raw string, no escapesstd::string path = R"(C:\temp\new)";Backslashes stay as they are
Printstd::cout << "x = " << x << '\n';#include <iostream>. '\n' rather than std::endl, which also flushes
Print with a format stringstd::println("{} is {} years old", name, age);C++23, GCC 14+. #include <print>
Format into a stringstd::string s = std::format("{:.2f}", price);C++20, GCC 13+. #include <format>
Pad and alignstd::format("{:>8}|{:<8}|{:^8}", a, b, c)Right, left and centred in 8 columns
Print to stderrstd::cerr << "error: " << msg << '\n';
Read one wordstd::string word; std::cin >> word;Stops at whitespace
Read a whole linestd::getline(std::cin, line);Drops the newline
Read a file line by linestd::ifstream in("data.txt"); for (std::string line; std::getline(in, line);) { }#include <fstream>. Check if (!in) after opening
Write a filestd::ofstream out("out.txt"); out << "hello\n";Closed when out goes out of scope

References vs pointers

A reference is another name for an object that already exists. It has to be bound when it is made, can never be null, and can never be pointed somewhere else. A pointer holds an address: it can be null, and it can be changed to point at something new.

TaskCodeNotes
Make a referenceint& r = x;r and x are the same int from now on
Assign through a referencer = y;Copies y's value into x. It does not rebind r
Read-only referenceconst std::string& s = name;
Let a function change the caller's variablevoid add_one(int& n) { ++n; } add_one(count);No & at the call site, unlike a pointer
Pass something big without copying itvoid print(const std::vector<int>& v);The default for anything bigger than a couple of ints
Loop without copyingfor (const auto& user : users) { }
Make a pointerint* p = &x;
Read or write through a pointer*p = 10;
Member through a pointerp->nameSame as (*p).name
Check for nullif (p != nullptr) { }Or if (p)
Point somewhere elsep = &y;A reference cannot do this
Cannot change the value pointed toconst int* p = &x;
Cannot point anywhere elseint* const p = &x;
Optional argumentvoid render(const Options* opts = nullptr);A pointer when nothing is a valid answer
Reference to a temporarystd::string&& s = make_name();Rvalue reference. The basis of move semantics

Reach for a reference by default, a raw pointer when null is a meaningful value or the target has to change, and a smart pointer when the pointer owns the object. A raw pointer in modern C++ should never own anything.

Functions

TaskCodeNotes
Declare a functionint add(int a, int b);Usually in a header, before any call
Define itint add(int a, int b) { return a + b; }
Default argumentvoid greet(std::string_view name = "world");Put it on the declaration, not the definition
Overload by parameter typesdouble area(double radius); int area(int w, int h);C++ picks one from the arguments. C cannot
Let the compiler work out the return typeauto square(int x) { return x * x; }
Return type after the parametersauto add(int a, int b) -> int;
Can run at compile timeconstexpr int square(int x) { return x * x; }And at run time when the argument is not a constant
Must run at compile timeconsteval int kib(int n) { return n * 1024; }C++20
Warn if the result is ignored[[nodiscard]] bool save();
Promise not to throwvoid swap(Widget& a, Widget& b) noexcept;
Return two valuesstd::pair<int, int> divide(int a, int b); auto [q, r] = divide(7, 2);Or return a small struct with named fields
Return a value or nothingstd::optional<User> find_user(int id);#include <optional>. return std::nullopt for nothing
Take a function as an argumentvoid each(const std::vector<int>& v, const std::function<void(int)>& fn);#include <functional>. A template is faster
Define a function in a headerinline int twice(int x) { return 2 * x; }inline stops the multiple-definition link error
Visible in this file onlynamespace { int helper() { return 1; } }Anonymous namespace. static works too
Group namesnamespace geometry { double area(double r); } geometry::area(2.0);
Program entry pointint main(int argc, char* argv[])Or int main(). Returns 0 if you leave out the return

Classes and constructors

TaskCodeNotes
Define a classclass Account { public: void deposit(double amount); private: double balance_ = 0; };Don't forget the ; after the closing brace
class or structstruct Point { int x; int y; };The only difference: struct members are public by default
Constructor with a member initialiser listAccount(std::string owner, double balance) : owner_(std::move(owner)), balance_(balance) {}Members are set in declaration order, whatever order you list them in
Default value for a memberint count_ = 0;Used by every constructor that does not set it
Keep the default constructorAccount() = default;
One constructor calling anotherPoint() : Point(0, 0) {}
Stop silent conversionsexplicit Meters(double value);Put explicit on every single-argument constructor
Create an objectAccount a{"Ada", 100.0};Lives until the end of the scope
Create by field namePoint p{.x = 1, .y = 2};C++20. Fields in declaration order
Method that does not change the objectdouble balance() const { return balance_; }Only const methods can be called on a const object
Shared by every objectstatic inline int instances = 0;Inside the class. inline lets you set it there
Run code when it is destroyed~Connection() { close(); }The destructor. Never let it throw
Inheritclass Circle : public Shape { };
Method a subclass must providevirtual double area() const = 0;Pure virtual. Shape can no longer be created directly
Replace a base methoddouble area() const override { return 3.14 * r_ * r_; }override makes a typo a compile error
Base class destructorvirtual ~Shape() = default;Needed on any base you delete through a pointer
Stop further inheritanceclass Circle final : public Shape { };
Compare with ==bool operator==(const Point&) const = default;C++20. Compares every member
Compare with < > <= >=auto operator<=>(const Point&) const = default;C++20. #include <compare>
Print with <<friend std::ostream& operator<<(std::ostream& os, const Point& p) { return os << p.x << ',' << p.y; }
Forbid copyingWidget(const Widget&) = delete; Widget& operator=(const Widget&) = delete;

Rule of zero: if every member cleans up after itself (std::string, std::vector, std::unique_ptr), write no destructor, copy or move functions at all. The compiler generates correct ones.

RAII and smart pointers

RAII (resource acquisition is initialisation) ties a resource to an object: the constructor acquires it, the destructor releases it, and the destructor runs however the scope ends, by return or by exception. Smart pointers are RAII for heap memory. Everything here needs #include <memory> unless it says otherwise.

TaskCodeNotes
Own an object on the heapauto w = std::make_unique<Widget>(42);Deleted automatically when w goes out of scope
Use itw->run(); Widget& ref = *w;
Hand ownership to another variableauto other = std::move(w);w is null afterwards. A unique_ptr cannot be copied
Own an arrayauto buf = std::make_unique<int[]>(n);Zeroed. A std::vector is usually better
Hold a subclass through its basestd::unique_ptr<Shape> s = std::make_unique<Circle>(2.0);Needs a virtual destructor on Shape
A list of owned objectsstd::vector<std::unique_ptr<Shape>> shapes;
Shared ownershipauto cfg = std::make_shared<Config>();Deleted when the last shared_ptr to it goes
Share itauto copy = cfg; cfg.use_count()2. Copying bumps an atomic counter
Watch without owningstd::weak_ptr<Config> watcher = cfg;Breaks shared_ptr cycles such as parent and child
Use a weak_ptrif (auto sp = watcher.lock()) { sp->reload(); }lock() is null if the object is gone
Get a plain pointerWidget* raw = w.get();Borrowed. Never delete it
Delete it noww.reset();
Function that only uses the objectvoid draw(const Widget& w); draw(*ptr);Take a smart pointer only when ownership is part of the deal
Function that takes ownershipvoid adopt(std::unique_ptr<Widget> w); adopt(std::move(ptr));
RAII for a C handleauto close = [](std::FILE* f) { std::fclose(f); }; std::unique_ptr<std::FILE, decltype(close)> f(std::fopen("a.txt", "r"), close);The deleter only runs if fopen succeeded
Lock a mutex for this scopestd::scoped_lock lock(m);#include <mutex>. Unlocks at the closing brace
Manual new and deleteWidget* w = new Widget; delete w;Avoid. Every new needs exactly one delete, and new[] needs delete[]

Templates

TaskCodeNotes
Function templatetemplate <typename T> T largest(T a, T b) { return a > b ? a : b; }
Call itlargest(3, 7) largest<double>(3, 7.5)T is deduced unless the arguments disagree
Class templatetemplate <typename T> class Stack { std::vector<T> items_; };
Use itStack<int> s;
Let the arguments pick the typestd::vector v{1, 2, 3}; std::pair p{1, 2.5};Class template argument deduction
Value as a template parametertemplate <std::size_t N> struct Buffer { char data[N]; };
Default template argumenttemplate <typename T = int> struct Counter { T value{}; };
Short form for a generic functionauto twice(auto x) { return x * 2; }C++20. Each auto is a template parameter
Only accept certain typestemplate <std::integral T> T gcd(T a, T b);C++20. #include <concepts>
requires clausetemplate <typename T> requires std::floating_point<T> T half(T x) { return x / 2; }C++20
Define a concepttemplate <typename T> concept Printable = requires(std::ostream& os, T x) { os << x; };C++20
Any number of argumentstemplate <typename... Args> void log(const Args&... args) { (std::cout << ... << args) << '\n'; }Fold expression
Count the argumentssizeof...(Args)
Branch on the type at compile timeif constexpr (std::is_integral_v<T>) { }The other branch is not compiled for that T
Ask about a typestd::is_same_v<T, int> std::is_pointer_v<T>#include <type_traits>
Special version for one typetemplate <> struct Counter<bool> { bool value = false; };Full specialisation
Check something at compile timestatic_assert(sizeof(T) <= 16, "T is too big");The message is optional

A template's full definition has to be visible wherever it is used, so templates live in headers. Putting the body in a .cpp file is the classic cause of an undefined reference error at link time.

STL containers

TaskCodeNotes
Growable arraystd::vector<int> v{1, 2, 3};#include <vector>. The default choice
Add to the endv.push_back(4); names.emplace_back("Ada");emplace_back builds it in place
Read by indexv[0] v.at(0)at() throws std::out_of_range, [] does not check
First, last, sizev.front() v.back() v.size() v.empty()
Remove the last onev.pop_back();
Insert in the middlev.insert(v.begin() + 1, 99);Shifts everything after it
Remove every matching valuestd::erase(v, 0); std::erase_if(v, [](int x) { return x < 0; });C++20
Make room up frontv.reserve(1000);Avoids repeated reallocation
Empty itv.clear();
Fixed-size arraystd::array<int, 3> a{1, 2, 3};#include <array>. Size is part of the type
Pass any contiguous rangeint sum(std::span<const int> nums);C++20. #include <span>. Takes a vector, array or C array
Key to value, sorted by keystd::map<std::string, int> ages{{"Ada", 36}};#include <map>. O(log n)
Key to value, hashedstd::unordered_map<std::string, int> counts;#include <unordered_map>. O(1) on average, no order
Insert or updatecounts[word]++;[] inserts a zero first if the key is missing
Look up without insertingif (auto it = m.find(key); it != m.end()) { use(it->second); }
Is the key therem.contains(key)C++20. m.count(key) before that
Insert only if missingm.try_emplace(key, 0);
Remove a keym.erase(key);
Unique valuesstd::set<int> s{3, 1, 2}; s.insert(4);#include <set>. Sorted. std::unordered_set is hashed
Add or remove at both endsstd::deque<int> d; d.push_front(0); d.push_back(9);#include <deque>
Stack and queuestd::stack<int> st; st.push(1); st.top(); st.pop();std::queue has front() instead of top()
Always get the smallest nextstd::priority_queue<int, std::vector<int>, std::greater<>> pq;#include <queue>. The default gives the largest
Two values togetherstd::pair<std::string, int> p{"Ada", 36}; p.first; p.second;
Several values togetherstd::tuple<int, double, char> t{1, 2.0, 'c'}; std::get<0>(t);#include <tuple>

Adding to a vector can move every element to new memory, which invalidates every pointer, reference and iterator into it. Don't push_back while looping over the same vector.

STL algorithms and ranges

The std::ranges versions take the whole container, so there is no begin and end to get wrong. #include <algorithm> for most of them, <numeric> for accumulate and iota, and <ranges> for the views.

TaskCodeNotes
Sortstd::ranges::sort(v);C++20. std::sort(v.begin(), v.end()) before that
Sort largest firststd::ranges::sort(v, std::greater{});
Sort by a fieldstd::ranges::sort(people, {}, &Person::age);C++20 projection
Sort with your own rulestd::ranges::sort(v, [](const auto& a, const auto& b) { return a.size() < b.size(); });Return true if a goes first. Never true for equal items
Sort and keep equal items in orderstd::ranges::stable_sort(v);
Find a valueauto it = std::ranges::find(v, 42); if (it != v.end()) { }
Find the first matchauto it = std::ranges::find_if(v, [](int x) { return x > 10; });
Does it containstd::ranges::contains(v, 42)C++23, GCC 13+
Countstd::ranges::count(v, 0) std::ranges::count_if(v, is_even)
Any, all or nonestd::ranges::any_of(v, is_even)all_of and none_of too
Smallest and largeststd::ranges::min(v) std::ranges::max(v)Undefined on an empty range
Where the largest isauto it = std::ranges::max_element(v);
Add them upstd::accumulate(v.begin(), v.end(), 0)The 0 sets the result type. Use 0.0 for doubles
Change every elementstd::ranges::transform(v, v.begin(), [](int x) { return x * 2; });
Reversestd::ranges::reverse(v);
Remove duplicatesstd::ranges::sort(v); v.erase(std::unique(v.begin(), v.end()), v.end());unique only removes neighbours, so sort first
Binary search a sorted rangestd::ranges::binary_search(v, 42)lower_bound gives the position
Fill with 1, 2, 3 and so onstd::iota(v.begin(), v.end(), 1);
Copy onto the end of anotherstd::ranges::copy(src, std::back_inserter(dst));#include <iterator>
Filter and map, lazilyauto sq = v | std::views::filter(is_even) | std::views::transform(square);C++20. Nothing runs until you loop over it
Collect a view into a vectorauto out = sq | std::ranges::to<std::vector>();C++23, GCC 14+
Loop over 0 to 9for (int i : std::views::iota(0, 10)) { }C++20
Loop with the indexfor (auto [i, x] : std::views::enumerate(v)) { }C++23, GCC 13+
Loop over two at oncefor (auto [a, b] : std::views::zip(xs, ys)) { }C++23, GCC 13+. Stops at the shorter one
Shufflestd::ranges::shuffle(v, std::mt19937{std::random_device{}()});#include <random>

Lambdas

A lambda is a function you write inline, usually to hand to an algorithm. The square brackets list what it captures from the surrounding scope.

TaskCodeNotes
Write one and call itauto add = [](int a, int b) { return a + b; }; add(2, 3);
Capture a copy[limit](int x) { return x < limit; }Copied when the lambda is made
Capture by reference[&total](int x) { total += x; }Sees and changes the real variable
Capture everything used, by copy[=]
Capture everything used, by reference[&]Fine for a lambda used on the spot, risky for one stored
Capture the object in a method[this] [*this][*this] copies the whole object
Capture a new variable[count = 0]() mutable { return ++count; }mutable lets it change its copies
Move something into it[p = std::move(ptr)] { p->run(); }The only way to capture a unique_ptr
Say the return type[](int x) -> double { return x / 2.0; }
Take any type[](const auto& x) { std::cout << x; }A generic lambda
Name the type[]<typename T>(const std::vector<T>& v) { return v.size(); }C++20
Call itselfauto fib = [](this auto self, int n) -> int { return n < 2 ? n : self(n - 1) + self(n - 2); };C++23, GCC 14+. Deducing this
Initialise a const in several stepsconst int limit = [&] { return fast ? 10 : 100; }();Called immediately
Store it for laterstd::function<int(int)> f = [](int x) { return x + 1; };#include <functional>. auto is cheaper when you can use it
Pass it to an algorithmstd::ranges::count_if(v, [](int x) { return x > 10; })

A lambda that captures by reference and outlives the scope it was made in, stored in a std::function, a thread or a callback, holds dangling references. Capture by copy anything the lambda might outlive.

Move semantics

Moving hands an object's resources, a heap buffer or a file handle, to another object instead of copying them. The object you moved from is left valid but unspecified: you can assign to it or destroy it, but don't read it until you have.

TaskCodeNotes
Move instead of copystd::string b = std::move(a);#include <utility>. std::move only casts, the move happens in b's constructor
Move into a containernames.push_back(std::move(name));
Build it in place insteadpeople.emplace_back("Ada", 36);No temporary to move at all
Take a value you will keepPerson(std::string name) : name_(std::move(name)) {}By value then move: one copy from a variable, none from a temporary
Return a localreturn result;Moved or elided for you. return std::move(result) can block the elision
Write a move constructorBuffer(Buffer&& other) noexcept : data_(std::exchange(other.data_, nullptr)) {}Take the resources, leave other empty
Write a move assignmentBuffer& operator=(Buffer&& other) noexcept { swap(other); return *this; }
Ask for the default movesWidget(Widget&&) = default; Widget& operator=(Widget&&) = default;
Mark moves noexceptBuffer(Buffer&&) noexcept;A vector that grows copies elements whose move might throw
Swap two valuesstd::swap(a, b);
Set a new value, return the old oneauto old = std::exchange(x, 0);
Pass an argument on unchangedtemplate <typename T> void wrap(T&& arg) { use(std::forward<T>(arg)); }Perfect forwarding. T&& here is a forwarding reference
Types you can move but not copystd::unique_ptr std::thread std::ifstream

Rule of five: if a class needs a custom destructor, copy constructor, copy assignment, move constructor or move assignment, it almost certainly needs all five. Better still, hold the resource in a member that manages itself and write none of them.

Modern C++: auto, range-for and structured bindings

TaskCodeNotes
Infer a variable's typeauto it = users.find(id);Saves writing std::map<std::string, User>::iterator
auto drops references and constauto copy = v[0]; auto& ref = v[0]; const auto& view = v[0];Add & when you don't want a copy
Loop over a containerfor (const auto& name : names) { }Read-only and no copies
Change every element in a loopfor (auto& x : v) { x *= 2; }
Loop with a setup statementfor (auto items = load(); const auto& item : items) { }C++20
Unpack a pair, tuple or structauto [name, age] = person;Structured bindings
Loop over a map's keys and valuesfor (const auto& [key, value] : ages) { }
if with a setup statementif (auto it = m.find(k); it != m.end()) { }it only exists inside the if and else
switch with a setup statementswitch (auto c = next(); c) { }
A value that might be missingstd::optional<int> port; port.value_or(8080);#include <optional>. if (port) then *port
One of several typesstd::variant<int, std::string> v = 42;#include <variant>. A type-safe union
Check and read a variantstd::holds_alternative<int>(v) std::get<int>(v)get throws std::bad_variant_access on the wrong type
Handle every type in a variantstd::visit([](const auto& x) { std::cout << x; }, v);
A value or an errorstd::expected<int, std::string> parse(std::string_view s);C++23. #include <expected>
Return the errorreturn std::unexpected("not a number");Caller checks if (result), then *result or result.error()
Silence an unused warning[[maybe_unused]] int debug_count = 0;

Exceptions and errors

TaskCodeNotes
Throwthrow std::runtime_error("file not found");#include <stdexcept>
Catchtry { load(); } catch (const std::exception& e) { std::cerr << e.what() << '\n'; }Always catch by const reference
Catch specific types firstcatch (const std::out_of_range& e) { } catch (const std::exception& e) { }The first matching catch wins
Catch anythingcatch (...) { }
Rethrow the same exceptionthrow;Inside a catch block. throw e; would slice it
Your own exception typeclass ParseError : public std::runtime_error { public: using std::runtime_error::runtime_error; };Inherits the string constructor
Common standard exceptionsstd::invalid_argument std::out_of_range std::runtime_error
Check an assumption while developingassert(count > 0);#include <cassert>. Removed when built with -DNDEBUG
Check at compile timestatic_assert(sizeof(int) == 4);

An exception that escapes main, or a destructor, or a noexcept function, ends the program with std::terminate. Catch at the boundary where you can actually do something about the failure, and let RAII clean up everything in between.

Counting words, start to finish

Strings, a hash map, a sort with a lambda and structured bindings in one program. It prints the three most common words, ties broken alphabetically.

#include <algorithm>
#include <print>
#include <ranges>
#include <sstream>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
 
int main()
{
    std::string text = "the cat sat on the mat and the cat slept";
 
    std::unordered_map<std::string, int> counts;
    std::istringstream words(text);
    for (std::string word; words >> word;) {
        ++counts[word];  // a missing key starts at 0
    }
 
    // A hash map has no order, so copy it out to sort it:
    // most frequent first, then A to Z.
    std::vector<std::pair<std::string, int>> ranked(counts.begin(), counts.end());
    std::ranges::sort(ranked, [](const auto& a, const auto& b) {
        if (a.second != b.second) {
            return a.second > b.second;
        }
        return a.first < b.first;
    });
 
    for (const auto& [word, count] : ranked | std::views::take(3)) {
        std::println("{:<5} {}", word, count);
    }
    // the   3
    // cat   2
    // and   1
}

Build it with the warnings and sanitisers on while you are still changing it:

g++ -std=c++23 -Wall -Wextra -g -fsanitize=address,undefined words.cpp -o words
./words

The standard does not say which algorithm std::sort uses, only that it is O(n log n). GCC's is introsort: a quick sort that falls back to heap sort if it recurses too deep, finished off with insertion sort on the small pieces. std::stable_sort is a merge sort. All four are on the site as step-through visualisations.

Classes and smart pointers, start to finish

An abstract base class, two subclasses and a vector that owns them. There is no delete anywhere: the unique_ptrs free every shape when shapes goes out of scope.

#include <memory>
#include <numbers>
#include <print>
#include <string>
#include <vector>
 
class Shape {
public:
    virtual ~Shape() = default;  // deleting through a Shape* needs this
    virtual double area() const = 0;
    virtual std::string name() const = 0;
};
 
class Circle : public Shape {
public:
    explicit Circle(double radius) : radius_(radius) {}
    double area() const override { return std::numbers::pi * radius_ * radius_; }
    std::string name() const override { return "circle"; }
 
private:
    double radius_;
};
 
class Rectangle : public Shape {
public:
    Rectangle(double width, double height) : width_(width), height_(height) {}
    double area() const override { return width_ * height_; }
    std::string name() const override { return "rectangle"; }
 
private:
    double width_;
    double height_;
};
 
int main()
{
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(1.0));
    shapes.push_back(std::make_unique<Rectangle>(3.0, 4.0));
 
    double total = 0;
    for (const auto& shape : shapes) {
        std::println("{:<10} {:.2f}", shape->name(), shape->area());
        total += shape->area();
    }
    std::println("{:<10} {:.2f}", "total", total);
    // circle     3.14
    // rectangle  12.00
    // total      15.14
}

The vector holds pointers rather than Shape values because a Shape value can only ever be a Shape. Copying a Circle into one keeps the base part and throws the rest away, which is called slicing.

Writing a class that can be moved

A class that owns raw memory has to say what copying and moving mean. This one follows the rule of five. In real code, a std::vector<int> member would do all of it for you, which is the rule of zero: this is what that rule saves you writing.

#include <algorithm>
#include <cstddef>
#include <print>
#include <utility>
 
class Buffer {
public:
    explicit Buffer(std::size_t size) : data_(new int[size]{}), size_(size) {}
 
    ~Buffer() { delete[] data_; }
 
    // Copy: allocate new memory and copy every element into it.
    Buffer(const Buffer& other) : data_(new int[other.size_]), size_(other.size_)
    {
        std::copy(other.data_, other.data_ + size_, data_);
    }
 
    Buffer& operator=(const Buffer& other)
    {
        Buffer copy(other);  // copy first, so a failed allocation changes nothing
        swap(copy);
        return *this;
    }
 
    // Move: take the other buffer's memory and leave it empty. No allocation.
    Buffer(Buffer&& other) noexcept
        : data_(std::exchange(other.data_, nullptr)),
          size_(std::exchange(other.size_, 0))
    {
    }
 
    Buffer& operator=(Buffer&& other) noexcept
    {
        Buffer moved(std::move(other));
        swap(moved);
        return *this;
    }
 
    void swap(Buffer& other) noexcept
    {
        std::swap(data_, other.data_);
        std::swap(size_, other.size_);
    }
 
    std::size_t size() const { return size_; }
 
private:
    int* data_;
    std::size_t size_;
};
 
int main()
{
    Buffer a(1000);
    Buffer b = a;             // copy: two separate arrays
    Buffer c = std::move(a);  // move: c takes a's array, a is left empty
 
    std::println("{} {} {}", a.size(), b.size(), c.size());  // 0 1000 1000
}

Reading a.size() after the move is only safe because Buffer promises that a moved-from buffer is empty. Standard library types make no such promise for most of their operations, so treat a moved-from std::string or std::vector as something to assign to, not read.

Templates and concepts, start to finish

One function that works for every numeric type, and a concept that turns a wrong type into a one-line error instead of a page of template noise.

#include <concepts>
#include <print>
#include <vector>
 
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
 
template <Numeric T>
T average(const std::vector<T>& values)
{
    if (values.empty()) {
        return T{};
    }
    T total{};
    for (T value : values) {
        total += value;
    }
    return total / static_cast<T>(values.size());
}
 
int main()
{
    std::vector<int> ints{1, 2, 3, 4};
    std::vector<double> doubles{1.5, 3.0};
 
    std::println("{}", average(ints));     // 2, because int division truncates
    std::println("{}", average(doubles));  // 2.25
    // average(std::vector<std::string>{"a"});  // error: std::string is not Numeric
}

Templates are compiled for each type they are used with, so average<int> and average<double> are two separate functions in the program. That is why a template's body has to sit in a header, where every file that calls it can see it.

Gotchas

The mistakes that turn up in almost every C++ codebase at some point.

Looks rightWhat actually happensDo this instead
std::vector<int> v(3); vs v{3}(3) makes three zeros, {3} makes one element holding 3Braces for a list of values, brackets for a count
Widget w();Declares a function called w that returns a WidgetWidget w; or Widget w{};
if (counts[key] > 0)[] on a map inserts the key if it is missingcounts.contains(key) or find
for (auto x : v) { x *= 2; }Doubles a copy of each element, so v never changesfor (auto& x : v)
v.push_back(x); while looping over vThe vector can move to new memory mid-loop, and the loop reads freed memoryLoop by index, or collect changes and add them after
std::string_view sv = name + "!";Points into a temporary string that is destroyed at the ;Keep the result in a std::string
Returning a reference to a local variableThe local is destroyed on return, so the caller gets a dangling referenceReturn by value. Moves and elision make it cheap
Shape s = circle;Slicing: copies only the Shape partHold subclasses through Shape& or std::unique_ptr<Shape>
Deleting a subclass through a base pointer with no virtual destructorOnly the base destructor runs, which is undefined behaviourvirtual ~Shape() = default;
Reading a string after std::move(s)Valid but unspecified, often emptyAssign a new value before using it again
std::accumulate(v.begin(), v.end(), 0) on doublesThe 0 makes the total an int, truncating as it goesStart from 0.0
for (int i = 0; i < v.size() - 1; i++) on an empty vectorv.size() - 1 wraps round to a huge unsigned numberi + 1 < v.size(), or std::ssize(v) - 1
std::cin >> n; then std::getline(std::cin, line);getline reads the newline left behind and returns an empty linestd::getline(std::cin >> std::ws, line);
std::format fails to compile with GCCGCC compiles as C++17 unless told otherwiseAdd -std=c++20 or -std=c++23

Common questions

Which version of C++ does this cheat sheet cover?

C++23, the current ISO standard (ISO/IEC 14882:2024), checked with GCC 15. GCC still compiles as C++17 unless you tell it otherwise, so pass -std=c++23 or -std=c++20 to g++. Anything on the page that needs a standard newer than C++17 says so in the notes column, and library features that arrived late in GCC, such as std::print and std::ranges::to in GCC 14, name the GCC version too.

What is the difference between a pointer and a reference in C++?

A reference is another name for an object that already exists. It must be bound when it is declared, can never be null, and always refers to the same object. A pointer is a separate variable holding an address: it can be null, and it can be changed to point at something else. Use a const reference to pass something without copying it, a reference to let a function change the caller's variable, and a pointer when having nothing is a valid answer.

When should I use unique_ptr and when should I use shared_ptr?

Use std::unique_ptr by default. It has one owner, costs nothing over a raw pointer, and its ownership can be moved. Use std::shared_ptr only when several parts of the program genuinely need to keep the same object alive and no single one of them outlives the rest. Shared ownership costs a reference count and makes it harder to tell when the object dies. If a function only uses an object, pass it a reference, not a smart pointer.

What is RAII in C++?

RAII stands for resource acquisition is initialisation. A class acquires a resource in its constructor, such as memory, a file or a lock, and releases it in its destructor. C++ runs destructors whenever an object goes out of scope, whether by return, break or exception, so the resource can never leak. std::vector, std::string, std::unique_ptr, std::ifstream and std::scoped_lock all work this way, which is why modern C++ rarely needs new, delete or explicit cleanup code.

What does std::move actually do?

Nothing, by itself. std::move is a cast that marks an object as something you are finished with, turning it into an rvalue. The move happens when that rvalue is passed to a move constructor or move assignment, which takes the object's resources instead of copying them. Calling std::move on a const object silently copies, because a const object cannot be moved from. After a move, the source is valid but unspecified, so assign to it before reading it again.

What is the difference between struct and class in C++?

Only the default access. Members and base classes of a struct are public unless you say otherwise, and those of a class are private. Everything else, constructors, methods, inheritance and templates, works the same. By convention, struct is used for plain bundles of data with public fields, and class for types that protect an invariant behind private members.

Why do I get an undefined reference error when I build my C++ program?

The linker found a declaration but no definition. The usual causes are a .cpp file left out of the g++ command, a function declared in a header but never defined, a template whose body is in a .cpp file instead of the header, a static data member declared but not defined, and building with gcc instead of g++ so the C++ standard library is not linked.

Should I learn C before C++?

You don't need to. Modern C++ is taught best on its own terms, with std::string, std::vector and smart pointers from day one, and learning C first can build habits like manual malloc and char arrays that C++ has better answers to. Learn C when you want to understand what is underneath, or when you are writing for embedded systems, kernels or libraries with a C interface. The C cheat sheet on this site covers the C side.

See all cheat sheets

Want this explained by a cat?

The videos cover the same ground in sixty seconds. If there is a tool you want a cheat sheet for next, ask.