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.
Searches the task, the command and the third column. Press / from anywhere on the page.
253 commands
Compiling and building
| Task | Command | Notes |
|---|---|---|
| Check which version you have | g++ --version | |
| Compile one file into a program | g++ -std=c++23 main.cpp -o main | Use g++, not gcc, so the C++ standard library is linked |
| Run it | ./main | |
| Pick a C++ standard | g++ -std=c++20 main.cpp -o main | Or c++17, c++23. GCC 15 defaults to gnu++17, so always set it |
| Turn on the warnings worth having | g++ -std=c++23 -Wall -Wextra -Wpedantic main.cpp -o main | |
| Treat every warning as an error | g++ -std=c++23 -Wall -Wextra -Werror main.cpp -o main | |
| Build for a debugger | g++ -std=c++23 -g -O0 main.cpp -o main | |
| Build optimised | g++ -std=c++23 -O2 main.cpp -o main | -O3 is more aggressive, -Os favours size |
| Catch memory bugs as they happen | g++ -std=c++23 -g -fsanitize=address,undefined main.cpp -o main | Out-of-bounds reads, use after free, overflow |
| Bounds-check operator[] and iterators | g++ -std=c++23 -D_GLIBCXX_ASSERTIONS main.cpp -o main | libstdc++ only. Cheap enough to leave on in debug builds |
| Compile several files into one program | g++ -std=c++23 main.cpp shape.cpp -o app | |
| Compile only, without linking | g++ -std=c++23 -c shape.cpp | Writes shape.o |
| Link object files | g++ main.o shape.o -o app | |
| Look for headers in another folder | g++ -std=c++23 -I include main.cpp -o main | |
| Link a library from another folder | g++ main.cpp -L lib -lfoo -o main | Finds lib/libfoo.so or lib/libfoo.a |
| Use clang instead | clang++ -std=c++23 main.cpp -o main | Takes the same flags |
| Configure a CMake project | cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug | |
| Build a CMake project | cmake --build build | |
| Step through it in a debugger | gdb ./main | break 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
| Task | Code | Notes |
|---|---|---|
| Integer | int n = 42; | 4 bytes on every common platform |
| Wider integer | long long big = 9'000'000'000LL; | 8 bytes. The ' is a digit separator |
| Floating point | double d = 3.14; float f = 3.14f; | double is the default for decimals |
| Boolean | bool done = false; | true and false are keywords |
| Character | char c = 'A'; | 1 byte |
| Exact-width integer | std::int32_t id = 7; std::uint64_t mask = 0; | #include <cstdint> |
| Size or count | std::size_t len = v.size(); | Unsigned. What .size() returns for the standard containers |
| Brace initialisation | int n{42}; | Refuses narrowing: int n{3.7} does not compile |
| Let the compiler infer the type | auto ratio = 1.5; | double. Needs an initialiser |
| Same type as another expression | decltype(x) y = x; | |
| Read-only variable | const double rate = 0.2; | |
| Compile-time constant | constexpr int max_users = 100; | Usable as an array size or template argument |
| Null pointer | int* p = nullptr; | Not NULL or 0 |
| Scoped enum | enum class Color { Red, Green, Blue }; Color c = Color::Red; | No silent conversion to int, unlike plain enum |
| Give a type another name | using Id = std::uint64_t; | The modern typedef |
| Convert to another type | static_cast<double>(total) / count | Cast one side, or the division is whole-number |
| Hex, octal and binary literals | 0xFF 0755 0b1010 | A leading 0 means octal, so 010 is 8 |
| Largest and smallest int | std::numeric_limits<int>::max() | #include <limits>. Also ::min() and ::lowest() |
| Maths constants | std::numbers::pi | C++20. #include <numbers> |
| size_t literal | auto i = 0uz; | C++23 |
Strings and I/O
| Task | Code | Notes |
|---|---|---|
| Make a string | std::string name = "Ada"; | #include <string>. Owns its characters and grows as needed |
| Length | name.size() | Also name.length(). empty() checks for zero |
| Join strings | std::string full = first + " " + last; | At least one side of each + must be a std::string |
| Append | name += "!"; name.push_back('?'); | |
| Compare | if (answer == "yes") { } | Compares text, unlike two C char pointers |
| One character | name[0] name.at(0) | at() throws std::out_of_range, [] does not check |
| Substring | s.substr(0, 3) | Start and length, not start and end |
| Find text | auto pos = s.find("cat"); | std::string::npos if it is not there |
| Contains | s.contains("cat") | C++23 |
| Starts or ends with | path.starts_with("/") file.ends_with(".cpp") | C++20 |
| Loop over the characters | for (char c : s) { } | |
| Number to text | std::to_string(42) | |
| Text to a number | int n = std::stoi(s); double d = std::stod(s); | Throws std::invalid_argument on bad input |
| Text to a number, no exceptions | auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), n); | #include <charconv>. Check ec == std::errc{} |
| Read-only view, no copy | void greet(std::string_view name); | #include <string_view>. Never let it outlive the string |
| Raw string, no escapes | std::string path = R"(C:\temp\new)"; | Backslashes stay as they are |
std::cout << "x = " << x << '\n'; | #include <iostream>. '\n' rather than std::endl, which also flushes | |
| Print with a format string | std::println("{} is {} years old", name, age); | C++23, GCC 14+. #include <print> |
| Format into a string | std::string s = std::format("{:.2f}", price); | C++20, GCC 13+. #include <format> |
| Pad and align | std::format("{:>8}|{:<8}|{:^8}", a, b, c) | Right, left and centred in 8 columns |
| Print to stderr | std::cerr << "error: " << msg << '\n'; | |
| Read one word | std::string word; std::cin >> word; | Stops at whitespace |
| Read a whole line | std::getline(std::cin, line); | Drops the newline |
| Read a file line by line | std::ifstream in("data.txt"); for (std::string line; std::getline(in, line);) { } | #include <fstream>. Check if (!in) after opening |
| Write a file | std::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.
| Task | Code | Notes |
|---|---|---|
| Make a reference | int& r = x; | r and x are the same int from now on |
| Assign through a reference | r = y; | Copies y's value into x. It does not rebind r |
| Read-only reference | const std::string& s = name; | |
| Let a function change the caller's variable | void add_one(int& n) { ++n; } add_one(count); | No & at the call site, unlike a pointer |
| Pass something big without copying it | void print(const std::vector<int>& v); | The default for anything bigger than a couple of ints |
| Loop without copying | for (const auto& user : users) { } | |
| Make a pointer | int* p = &x; | |
| Read or write through a pointer | *p = 10; | |
| Member through a pointer | p->name | Same as (*p).name |
| Check for null | if (p != nullptr) { } | Or if (p) |
| Point somewhere else | p = &y; | A reference cannot do this |
| Cannot change the value pointed to | const int* p = &x; | |
| Cannot point anywhere else | int* const p = &x; | |
| Optional argument | void render(const Options* opts = nullptr); | A pointer when nothing is a valid answer |
| Reference to a temporary | std::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
| Task | Code | Notes |
|---|---|---|
| Declare a function | int add(int a, int b); | Usually in a header, before any call |
| Define it | int add(int a, int b) { return a + b; } | |
| Default argument | void greet(std::string_view name = "world"); | Put it on the declaration, not the definition |
| Overload by parameter types | double area(double radius); int area(int w, int h); | C++ picks one from the arguments. C cannot |
| Let the compiler work out the return type | auto square(int x) { return x * x; } | |
| Return type after the parameters | auto add(int a, int b) -> int; | |
| Can run at compile time | constexpr int square(int x) { return x * x; } | And at run time when the argument is not a constant |
| Must run at compile time | consteval int kib(int n) { return n * 1024; } | C++20 |
| Warn if the result is ignored | [[nodiscard]] bool save(); | |
| Promise not to throw | void swap(Widget& a, Widget& b) noexcept; | |
| Return two values | std::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 nothing | std::optional<User> find_user(int id); | #include <optional>. return std::nullopt for nothing |
| Take a function as an argument | void each(const std::vector<int>& v, const std::function<void(int)>& fn); | #include <functional>. A template is faster |
| Define a function in a header | inline int twice(int x) { return 2 * x; } | inline stops the multiple-definition link error |
| Visible in this file only | namespace { int helper() { return 1; } } | Anonymous namespace. static works too |
| Group names | namespace geometry { double area(double r); } geometry::area(2.0); | |
| Program entry point | int main(int argc, char* argv[]) | Or int main(). Returns 0 if you leave out the return |
Classes and constructors
| Task | Code | Notes |
|---|---|---|
| Define a class | class Account { public: void deposit(double amount); private: double balance_ = 0; }; | Don't forget the ; after the closing brace |
| class or struct | struct Point { int x; int y; }; | The only difference: struct members are public by default |
| Constructor with a member initialiser list | Account(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 member | int count_ = 0; | Used by every constructor that does not set it |
| Keep the default constructor | Account() = default; | |
| One constructor calling another | Point() : Point(0, 0) {} | |
| Stop silent conversions | explicit Meters(double value); | Put explicit on every single-argument constructor |
| Create an object | Account a{"Ada", 100.0}; | Lives until the end of the scope |
| Create by field name | Point p{.x = 1, .y = 2}; | C++20. Fields in declaration order |
| Method that does not change the object | double balance() const { return balance_; } | Only const methods can be called on a const object |
| Shared by every object | static 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 |
| Inherit | class Circle : public Shape { }; | |
| Method a subclass must provide | virtual double area() const = 0; | Pure virtual. Shape can no longer be created directly |
| Replace a base method | double area() const override { return 3.14 * r_ * r_; } | override makes a typo a compile error |
| Base class destructor | virtual ~Shape() = default; | Needed on any base you delete through a pointer |
| Stop further inheritance | class 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 copying | Widget(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.
| Task | Code | Notes |
|---|---|---|
| Own an object on the heap | auto w = std::make_unique<Widget>(42); | Deleted automatically when w goes out of scope |
| Use it | w->run(); Widget& ref = *w; | |
| Hand ownership to another variable | auto other = std::move(w); | w is null afterwards. A unique_ptr cannot be copied |
| Own an array | auto buf = std::make_unique<int[]>(n); | Zeroed. A std::vector is usually better |
| Hold a subclass through its base | std::unique_ptr<Shape> s = std::make_unique<Circle>(2.0); | Needs a virtual destructor on Shape |
| A list of owned objects | std::vector<std::unique_ptr<Shape>> shapes; | |
| Shared ownership | auto cfg = std::make_shared<Config>(); | Deleted when the last shared_ptr to it goes |
| Share it | auto copy = cfg; cfg.use_count() | 2. Copying bumps an atomic counter |
| Watch without owning | std::weak_ptr<Config> watcher = cfg; | Breaks shared_ptr cycles such as parent and child |
| Use a weak_ptr | if (auto sp = watcher.lock()) { sp->reload(); } | lock() is null if the object is gone |
| Get a plain pointer | Widget* raw = w.get(); | Borrowed. Never delete it |
| Delete it now | w.reset(); | |
| Function that only uses the object | void draw(const Widget& w); draw(*ptr); | Take a smart pointer only when ownership is part of the deal |
| Function that takes ownership | void adopt(std::unique_ptr<Widget> w); adopt(std::move(ptr)); | |
| RAII for a C handle | auto 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 scope | std::scoped_lock lock(m); | #include <mutex>. Unlocks at the closing brace |
| Manual new and delete | Widget* w = new Widget; delete w; | Avoid. Every new needs exactly one delete, and new[] needs delete[] |
Templates
| Task | Code | Notes |
|---|---|---|
| Function template | template <typename T> T largest(T a, T b) { return a > b ? a : b; } | |
| Call it | largest(3, 7) largest<double>(3, 7.5) | T is deduced unless the arguments disagree |
| Class template | template <typename T> class Stack { std::vector<T> items_; }; | |
| Use it | Stack<int> s; | |
| Let the arguments pick the type | std::vector v{1, 2, 3}; std::pair p{1, 2.5}; | Class template argument deduction |
| Value as a template parameter | template <std::size_t N> struct Buffer { char data[N]; }; | |
| Default template argument | template <typename T = int> struct Counter { T value{}; }; | |
| Short form for a generic function | auto twice(auto x) { return x * 2; } | C++20. Each auto is a template parameter |
| Only accept certain types | template <std::integral T> T gcd(T a, T b); | C++20. #include <concepts> |
| requires clause | template <typename T> requires std::floating_point<T> T half(T x) { return x / 2; } | C++20 |
| Define a concept | template <typename T> concept Printable = requires(std::ostream& os, T x) { os << x; }; | C++20 |
| Any number of arguments | template <typename... Args> void log(const Args&... args) { (std::cout << ... << args) << '\n'; } | Fold expression |
| Count the arguments | sizeof...(Args) | |
| Branch on the type at compile time | if constexpr (std::is_integral_v<T>) { } | The other branch is not compiled for that T |
| Ask about a type | std::is_same_v<T, int> std::is_pointer_v<T> | #include <type_traits> |
| Special version for one type | template <> struct Counter<bool> { bool value = false; }; | Full specialisation |
| Check something at compile time | static_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
| Task | Code | Notes |
|---|---|---|
| Growable array | std::vector<int> v{1, 2, 3}; | #include <vector>. The default choice |
| Add to the end | v.push_back(4); names.emplace_back("Ada"); | emplace_back builds it in place |
| Read by index | v[0] v.at(0) | at() throws std::out_of_range, [] does not check |
| First, last, size | v.front() v.back() v.size() v.empty() | |
| Remove the last one | v.pop_back(); | |
| Insert in the middle | v.insert(v.begin() + 1, 99); | Shifts everything after it |
| Remove every matching value | std::erase(v, 0); std::erase_if(v, [](int x) { return x < 0; }); | C++20 |
| Make room up front | v.reserve(1000); | Avoids repeated reallocation |
| Empty it | v.clear(); | |
| Fixed-size array | std::array<int, 3> a{1, 2, 3}; | #include <array>. Size is part of the type |
| Pass any contiguous range | int sum(std::span<const int> nums); | C++20. #include <span>. Takes a vector, array or C array |
| Key to value, sorted by key | std::map<std::string, int> ages{{"Ada", 36}}; | #include <map>. O(log n) |
| Key to value, hashed | std::unordered_map<std::string, int> counts; | #include <unordered_map>. O(1) on average, no order |
| Insert or update | counts[word]++; | [] inserts a zero first if the key is missing |
| Look up without inserting | if (auto it = m.find(key); it != m.end()) { use(it->second); } | |
| Is the key there | m.contains(key) | C++20. m.count(key) before that |
| Insert only if missing | m.try_emplace(key, 0); | |
| Remove a key | m.erase(key); | |
| Unique values | std::set<int> s{3, 1, 2}; s.insert(4); | #include <set>. Sorted. std::unordered_set is hashed |
| Add or remove at both ends | std::deque<int> d; d.push_front(0); d.push_back(9); | #include <deque> |
| Stack and queue | std::stack<int> st; st.push(1); st.top(); st.pop(); | std::queue has front() instead of top() |
| Always get the smallest next | std::priority_queue<int, std::vector<int>, std::greater<>> pq; | #include <queue>. The default gives the largest |
| Two values together | std::pair<std::string, int> p{"Ada", 36}; p.first; p.second; | |
| Several values together | std::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.
| Task | Code | Notes |
|---|---|---|
| Sort | std::ranges::sort(v); | C++20. std::sort(v.begin(), v.end()) before that |
| Sort largest first | std::ranges::sort(v, std::greater{}); | |
| Sort by a field | std::ranges::sort(people, {}, &Person::age); | C++20 projection |
| Sort with your own rule | std::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 order | std::ranges::stable_sort(v); | |
| Find a value | auto it = std::ranges::find(v, 42); if (it != v.end()) { } | |
| Find the first match | auto it = std::ranges::find_if(v, [](int x) { return x > 10; }); | |
| Does it contain | std::ranges::contains(v, 42) | C++23, GCC 13+ |
| Count | std::ranges::count(v, 0) std::ranges::count_if(v, is_even) | |
| Any, all or none | std::ranges::any_of(v, is_even) | all_of and none_of too |
| Smallest and largest | std::ranges::min(v) std::ranges::max(v) | Undefined on an empty range |
| Where the largest is | auto it = std::ranges::max_element(v); | |
| Add them up | std::accumulate(v.begin(), v.end(), 0) | The 0 sets the result type. Use 0.0 for doubles |
| Change every element | std::ranges::transform(v, v.begin(), [](int x) { return x * 2; }); | |
| Reverse | std::ranges::reverse(v); | |
| Remove duplicates | std::ranges::sort(v); v.erase(std::unique(v.begin(), v.end()), v.end()); | unique only removes neighbours, so sort first |
| Binary search a sorted range | std::ranges::binary_search(v, 42) | lower_bound gives the position |
| Fill with 1, 2, 3 and so on | std::iota(v.begin(), v.end(), 1); | |
| Copy onto the end of another | std::ranges::copy(src, std::back_inserter(dst)); | #include <iterator> |
| Filter and map, lazily | auto 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 vector | auto out = sq | std::ranges::to<std::vector>(); | C++23, GCC 14+ |
| Loop over 0 to 9 | for (int i : std::views::iota(0, 10)) { } | C++20 |
| Loop with the index | for (auto [i, x] : std::views::enumerate(v)) { } | C++23, GCC 13+ |
| Loop over two at once | for (auto [a, b] : std::views::zip(xs, ys)) { } | C++23, GCC 13+. Stops at the shorter one |
| Shuffle | std::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.
| Task | Code | Notes |
|---|---|---|
| Write one and call it | auto 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 itself | auto 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 steps | const int limit = [&] { return fast ? 10 : 100; }(); | Called immediately |
| Store it for later | std::function<int(int)> f = [](int x) { return x + 1; }; | #include <functional>. auto is cheaper when you can use it |
| Pass it to an algorithm | std::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.
| Task | Code | Notes |
|---|---|---|
| Move instead of copy | std::string b = std::move(a); | #include <utility>. std::move only casts, the move happens in b's constructor |
| Move into a container | names.push_back(std::move(name)); | |
| Build it in place instead | people.emplace_back("Ada", 36); | No temporary to move at all |
| Take a value you will keep | Person(std::string name) : name_(std::move(name)) {} | By value then move: one copy from a variable, none from a temporary |
| Return a local | return result; | Moved or elided for you. return std::move(result) can block the elision |
| Write a move constructor | Buffer(Buffer&& other) noexcept : data_(std::exchange(other.data_, nullptr)) {} | Take the resources, leave other empty |
| Write a move assignment | Buffer& operator=(Buffer&& other) noexcept { swap(other); return *this; } | |
| Ask for the default moves | Widget(Widget&&) = default; Widget& operator=(Widget&&) = default; | |
| Mark moves noexcept | Buffer(Buffer&&) noexcept; | A vector that grows copies elements whose move might throw |
| Swap two values | std::swap(a, b); | |
| Set a new value, return the old one | auto old = std::exchange(x, 0); | |
| Pass an argument on unchanged | template <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 copy | std::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
| Task | Code | Notes |
|---|---|---|
| Infer a variable's type | auto it = users.find(id); | Saves writing std::map<std::string, User>::iterator |
| auto drops references and const | auto copy = v[0]; auto& ref = v[0]; const auto& view = v[0]; | Add & when you don't want a copy |
| Loop over a container | for (const auto& name : names) { } | Read-only and no copies |
| Change every element in a loop | for (auto& x : v) { x *= 2; } | |
| Loop with a setup statement | for (auto items = load(); const auto& item : items) { } | C++20 |
| Unpack a pair, tuple or struct | auto [name, age] = person; | Structured bindings |
| Loop over a map's keys and values | for (const auto& [key, value] : ages) { } | |
| if with a setup statement | if (auto it = m.find(k); it != m.end()) { } | it only exists inside the if and else |
| switch with a setup statement | switch (auto c = next(); c) { } | |
| A value that might be missing | std::optional<int> port; port.value_or(8080); | #include <optional>. if (port) then *port |
| One of several types | std::variant<int, std::string> v = 42; | #include <variant>. A type-safe union |
| Check and read a variant | std::holds_alternative<int>(v) std::get<int>(v) | get throws std::bad_variant_access on the wrong type |
| Handle every type in a variant | std::visit([](const auto& x) { std::cout << x; }, v); | |
| A value or an error | std::expected<int, std::string> parse(std::string_view s); | C++23. #include <expected> |
| Return the error | return 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
| Task | Code | Notes |
|---|---|---|
| Throw | throw std::runtime_error("file not found"); | #include <stdexcept> |
| Catch | try { load(); } catch (const std::exception& e) { std::cerr << e.what() << '\n'; } | Always catch by const reference |
| Catch specific types first | catch (const std::out_of_range& e) { } catch (const std::exception& e) { } | The first matching catch wins |
| Catch anything | catch (...) { } | |
| Rethrow the same exception | throw; | Inside a catch block. throw e; would slice it |
| Your own exception type | class ParseError : public std::runtime_error { public: using std::runtime_error::runtime_error; }; | Inherits the string constructor |
| Common standard exceptions | std::invalid_argument std::out_of_range std::runtime_error | |
| Check an assumption while developing | assert(count > 0); | #include <cassert>. Removed when built with -DNDEBUG |
| Check at compile time | static_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
./wordsThe 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 right | What actually happens | Do this instead |
|---|---|---|
std::vector<int> v(3); vs v{3} | (3) makes three zeros, {3} makes one element holding 3 | Braces for a list of values, brackets for a count |
Widget w(); | Declares a function called w that returns a Widget | Widget w; or Widget w{}; |
if (counts[key] > 0) | [] on a map inserts the key if it is missing | counts.contains(key) or find |
for (auto x : v) { x *= 2; } | Doubles a copy of each element, so v never changes | for (auto& x : v) |
v.push_back(x); while looping over v | The vector can move to new memory mid-loop, and the loop reads freed memory | Loop 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 variable | The local is destroyed on return, so the caller gets a dangling reference | Return by value. Moves and elision make it cheap |
Shape s = circle; | Slicing: copies only the Shape part | Hold subclasses through Shape& or std::unique_ptr<Shape> |
| Deleting a subclass through a base pointer with no virtual destructor | Only the base destructor runs, which is undefined behaviour | virtual ~Shape() = default; |
Reading a string after std::move(s) | Valid but unspecified, often empty | Assign a new value before using it again |
std::accumulate(v.begin(), v.end(), 0) on doubles | The 0 makes the total an int, truncating as it goes | Start from 0.0 |
for (int i = 0; i < v.size() - 1; i++) on an empty vector | v.size() - 1 wraps round to a huge unsigned number | i + 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 line | std::getline(std::cin >> std::ws, line); |
std::format fails to compile with GCC | GCC compiles as C++17 unless told otherwise | Add -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.
