askesis

C++ essentials: 50 cards on RAII, smart pointers, move semantics and the STL

Modern C++ in short snippets: references vs pointers, RAII, smart pointers, move semantics, STL containers and algorithms, const, templates and UB.

ProgrammingIntermediate50 cardsFree

Practice now

Study without an account

0 of 50 seen0 graduated
Reference vs pointer

Scheduled with the default Askesis timings. Progress is saved in this browser.

All 50 cards

  1. 1
    Reference vs pointer
    A reference is an alias that must be initialized, can’t be null and can’t be reseated; a pointer holds an address, can be null, can be reassigned, and needs * to reach the object
  2. 2
    What does this print?
    cpp
    int a = 1;
    int& r = a;
    r = 5;
    std::cout << a;
    5: r is another name for a, so assigning through it changes a
  3. 3
    Three ways to take a std::string parameter
    std::string s copies it; std::string& s lets the function modify the caller’s string; const std::string& s reads it without copying
    For read-only string parameters, std::string_view (C++17) is another option.
  4. 4
    Why nullptr instead of NULL or 0?
    nullptr (C++11) has its own type, std::nullptr_t, which converts only to pointer types; NULL is usually an integer constant, so f(NULL) can call f(int) instead of f(char*)
  5. 5
    What’s wrong with this function?
    cpp
    const std::string& name() {
        std::string s = "Ada";
        return s;
    }
    It returns a reference to a local that is destroyed when the function returns, so using the result is undefined behavior. Return std::string by value instead
    Returning by value is cheap thanks to move semantics and copy elision.
  6. 6
    What is RAII?
    “Resource acquisition is initialization”: an object acquires a resource in its constructor and releases it in its destructor, so cleanup happens automatically when the object goes out of scope, even if an exception is thrown
    Local objects are destroyed at the end of their scope, in reverse order of construction.
  7. 7
    Lock a mutex for the rest of the scope
    cpp
    {
        std::scoped_lock lock(mtx);
        shared.push_back(x);
    } // unlocked here, even if push_back throws
    std::scoped_lock is C++17; std::lock_guard<std::mutex> is the older equivalent.
  8. 8
    What is std::unique_ptr<T>?
    A smart pointer with sole ownership: it deletes the object when it is destroyed. It can be moved but not copied, and typically costs nothing over a raw pointer
  9. 9
    Create an object owned by a unique_ptr
    cpp
    auto w = std::make_unique<Widget>(42);
    std::make_unique (C++14) avoids a naked new, so there is never a moment when the object has no owner.
  10. 10
    Transfer ownership of a unique_ptr
    cpp
    auto a = std::make_unique<Widget>();
    auto b = std::move(a); // a is now null
    auto b = a; doesn’t compile, because a unique_ptr can’t be copied.
  11. 11
    What is std::shared_ptr<T>?
    A smart pointer with shared ownership: copies share a reference count, and the object is deleted when the last shared_ptr to it is destroyed
    Create it with std::make_shared<T>(…), which allocates the object and the count together.
  12. 12
    What is std::weak_ptr for?
    A non-owning reference to an object managed by shared_ptr. It breaks ownership cycles, and lock() returns a shared_ptr if the object still exists (or an empty one if not)
  13. 13
    Are raw pointers still OK in modern C++?
    As non-owning observers, yes. Avoid owning raw pointers with manual new and delete; let containers and smart pointers own memory
  14. 14
    What is move semantics?
    Transferring an object’s resources (like a string’s heap buffer) to another object instead of copying them, when the source is a temporary or no longer needed
    A move constructor has the signature T(T&& other) noexcept.
  15. 15
    What does std::move actually do?
    Nothing at runtime: it is a cast to an rvalue reference, which lets overload resolution pick a move constructor or move assignment
    std::move on a const object silently copies, because the move constructor takes a non-const T&&.
  16. 16
    What state is an object in after being moved from?
    Valid but unspecified: you can destroy it or assign it a new value, but shouldn’t rely on its contents
    A moved-from std::string is usually empty, but the standard doesn’t promise that.
  17. 17
    lvalue vs rvalue
    An lvalue has an identity you can take the address of, like a named variable; an rvalue is a temporary such as x + 1 or std::string("hi"). T&& parameters bind to rvalues
  18. 18
    Rule of zero vs rule of five
    Rule of five: if you define a destructor, copy or move constructor, or copy or move assignment, you probably need all five. Rule of zero (better): use members like std::vector and std::unique_ptr that manage themselves, and define none
  19. 19
    Why should move constructors be noexcept?
    When std::vector reallocates, it moves elements only if their move constructor can’t throw; otherwise it copies them (if it can) to keep its strong exception guarantee
  20. 20
    Which container should you reach for by default?
    std::vector: a contiguous, growable array with fast iteration and amortized O(1) push_back
  21. 21
    What’s wrong here?
    cpp
    std::vector<int> v{1, 2, 3};
    int& first = v[0];
    v.push_back(4);
    std::cout << first;
    If push_back reallocates, first refers to freed memory and reading it is undefined behavior. Growing a vector can invalidate all references, pointers and iterators to its elements
  22. 22
    std::map vs std::unordered_map
    std::map keeps keys sorted (a balanced tree, O(log n) operations); std::unordered_map is a hash table with average O(1) lookups and no ordering
  23. 23
    What is m.size() afterwards?
    cpp
    std::map<std::string, int> m;
    if (m["alice"] == 0) { /* … */ }
    1: operator[] inserts a value-initialized element when the key is missing
    To only look, use m.find(key), m.contains(key) (C++20) or m.at(key), which throws if the key is missing.
  24. 24
    v[i] vs v.at(i)
    v[i] doesn’t check bounds, so an out-of-range index is undefined behavior; v.at(i) throws std::out_of_range
  25. 25
    Why use std::array<int, 3> over int[3]?
    Same fixed size and no heap allocation, but it knows its size, can be copied and returned by value, works with algorithms, and doesn’t decay to a pointer
  26. 26
    The std::string_view gotcha
    A string_view (C++17) doesn’t own its characters. If it outlives the string it points into, such as a temporary, it dangles
    cpp
    std::string_view sv = std::string("temp"); // dangles immediately
  27. 27
    push_back vs emplace_back
    push_back takes an existing object and copies or moves it in; emplace_back forwards constructor arguments and builds the element in place
    cpp
    people.emplace_back("Ada", 36); // calls Person(std::string, int)
  28. 28
    Sort a vector ascending, then descending
    cpp
    std::ranges::sort(v);                              // C++20
    std::sort(v.begin(), v.end(), std::greater<>());   // descending
  29. 29
    Remove all zeros from a vector
    cpp
    std::erase(v, 0); // C++20
    
    // before C++20: the erase-remove idiom
    v.erase(std::remove(v.begin(), v.end(), 0), v.end());
    std::remove only shifts the kept elements forward and returns the new logical end; it never changes the size.
  30. 30
    Find a value in a vector
    cpp
    auto it = std::find(v.begin(), v.end(), 42);
    if (it != v.end()) {
        std::cout << "found at " << (it - v.begin());
    }
  31. 31
    Lambda capture lists
    [=] captures used variables by copy, [&] by reference, [x, &y] names them individually, and [this] captures the current object
    Capturing by reference in a lambda that outlives the scope, such as a stored callback, leaves dangling references.
  32. 32
    Loop over a std::vector<std::string> without copying each element
    cpp
    for (const auto& name : names) {
        std::cout << name << '\n';
    }
    for (auto name : names) copies every string.
  33. 33
    What’s wrong with this sum?
    cpp
    std::vector<double> prices{1.5, 2.5};
    auto total = std::accumulate(prices.begin(), prices.end(), 0);
    The initial value 0 is an int, so the sum is accumulated as an int, truncating at each step: total is 3, not 4.0. Pass 0.0
  34. 34
    const int* p vs int* const p
    Read the declaration right to left.
    const int* p: you can’t modify *p, but p can point elsewhere. int* const p: you can modify *p, but p can’t be reseated
  35. 35
    What does const after a member function mean, as in int size() const;?
    The function promises not to modify the object, and only const member functions can be called on a const object or through a const reference
  36. 36
    What does constexpr mean?
    The value or function can be evaluated at compile time; a constexpr variable must be initialized with a constant expression
    cpp
    constexpr int square(int x) { return x * x; }
    std::array<int, square(3)> grid; // size 9, computed at compile time
  37. 37
    A function template
    cpp
    template <typename T>
    T max_of(T a, T b) {
        return a > b ? a : b;
    }
    
    max_of(3, 7);      // T = int
    max_of(2.5, 1.0);  // T = double
  38. 38
    Why are templates usually defined in headers?
    The compiler instantiates a template separately for each set of type arguments, and it needs the full definition visible at the point of use to do that
  39. 39
    Constrain a template to integer types
    cpp
    template <std::integral T>
    T gcd(T a, T b) {
        return b == 0 ? a : gcd(b, a % b);
    }
    Concepts (C++20) give clear errors instead of a page of template failures; std::integral is in <concepts>.
  40. 40
    Does this copy the string?
    cpp
    std::vector<std::string> names = load();
    auto first = names[0];
    Yes: auto drops references (and top-level const), so first is a copy. Write auto& first or const auto& first to refer to the element
  41. 41
    What is undefined behavior?
    Code the standard places no requirements on. The compiler may assume it never happens, so the program might crash, give wrong results, or seem to work until it doesn’t
  42. 42
    Signed vs unsigned integer overflow
    Signed overflow (INT_MAX + 1) is undefined behavior; unsigned arithmetic wraps around modulo 2ⁿ
    Optimizers exploit this: x + 1 > x may be folded to true for a signed x.
  43. 43
    What does this print?
    cpp
    int x;
    std::cout << x;
    Anything: reading an uninitialized local variable is undefined behavior. Always initialize: int x = 0; or int x{};
    C++26 reclassifies it as “erroneous behavior”, which is still a bug.
  44. 44
    Common sources of undefined behavior
    Out-of-bounds access, dereferencing null or dangling pointers, use after free, double delete, signed overflow, reading uninitialized variables, and data races
    Sanitizers catch many of them: compile with -fsanitize=address,undefined.
  45. 45
    What is a data race?
    Two threads access the same non-atomic variable without synchronization, and at least one of them writes. It is undefined behavior; use std::mutex or std::atomic
  46. 46
    When does a base class need a virtual destructor?
    When objects may be deleted through a pointer to the base; without it, delete base_ptr on a derived object is undefined behavior
    Rule of thumb: a class with any virtual function should have a virtual destructor.
  47. 47
    What does Widget w(); declare?
    A function named w that returns a Widget, not an object (the “most vexing parse”). Write Widget w; or Widget w{};
  48. 48
    int x = 3.7; vs int x{3.7};
    The first silently truncates to 3; the second doesn’t compile, because brace initialization rejects narrowing conversions
  49. 49
    Return “a value or nothing” from a function
    cpp
    std::optional<User> find_user(int id);
    
    if (auto user = find_user(42)) {
        std::cout << user->name;
    }
    std::optional is C++17; value_or(fallback) supplies a default.
  50. 50
    What does override do?
    Asks the compiler to verify that the function overrides a base-class virtual function, turning a signature mismatch into a compile error
    cpp
    void draw() const override;

Frequently asked questions

Which C++ standard does the deck use?

Modern C++, mostly C++17 and C++20 idioms. Newer features are marked with their standard, such as std::make_unique (C++14), std::optional and string_view (C++17), and concepts and ranges (C++20).

Is this deck for C++ beginners?

It suits anyone who knows basic C++ syntax, or comes from C or another language, and wants the modern idioms: ownership, move semantics, the STL and the pitfalls behind undefined behavior.

Why so many cards on undefined behavior?

Because UB is where C++ differs most from safer languages: buggy code often compiles and appears to work. Knowing the common sources is the best defense, together with sanitizers.

Intermediate

JavaScript

Master the JavaScript every developer uses daily: scope, closures, equality, array methods, promises, async/await and the event loop.

50 cards
Intermediate

Advanced JS

Fifty advanced JavaScript cards: event-loop ordering puzzles, generators and async iteration, this binding, Proxy and Reflect, modules, AbortController and coercion edge cases.

50 cards
Intermediate

TypeScript

Learn the TypeScript you use every day: narrowing, generics, keyof, mapped and conditional types, utility types, satisfies and strict mode, with code on every card.

50 cards
Beginner

Python

Learn idiomatic Python fast: comprehensions, f-strings, args and kwargs, generators, decorators, context managers and the standard library.

50 cards
Beginner

Git & terminal

The Git commands and shell skills developers use every day: branching, rebasing, undoing mistakes, pipes, redirects, permissions and processes.

50 cards
Beginner

Java

Core Java with short snippets: equals vs ==, records, sealed types, collections, generics, exceptions, streams, lambdas and JVM basics.

50 cards
Beginner

C#

C# and .NET fundamentals with short snippets: value vs reference types, properties, records, nullable reference types, LINQ, async/await and DI.

50 cards
Beginner

Kotlin

Kotlin fundamentals with short snippets: null safety, val vs var, data and sealed classes, extension functions, scope functions and coroutines.

50 cards
Beginner

Swift

Swift fundamentals with short snippets: optionals, structs vs classes, protocols, enums with associated values, closures, ARC, async/await and SwiftUI.

50 cards
Beginner

Flutter

Flutter and Dart basics: null safety, futures and streams, stateless vs stateful widgets, setState, hot reload, keys, layout and state management.

50 cards
Beginner

PHP

Modern PHP 8 with short snippets: strict types, arrays, match, nullsafe and named arguments, enums, readonly, Composer, PDO and password_hash.

50 cards
Beginner

Ruby

Ruby fundamentals with short snippets: blocks, procs and lambdas, symbols, Enumerable, mixins, truthiness, method_missing, gems and Bundler.

50 cards
Beginner

Go

Go (Golang) fundamentals with short, idiomatic snippets: slices vs arrays, maps, defer, error wrapping, interfaces, goroutines, channels, select and context.

50 cards
Intermediate

Rust

Learn the Rust that the compiler keeps asking about: ownership, borrowing, lifetimes, Option and Result, traits, closures, smart pointers and async.

50 cards
Intermediate

Haskell

Haskell fundamentals with short code: types and type classes, pattern matching, laziness, Maybe and Either, folds, Functor, Applicative and Monad.

50 cards
Intermediate

Elixir

Elixir fundamentals with short snippets: immutability, pattern matching, the pipe operator, Enum vs Stream, processes, GenServer, supervisors and mix.

50 cards
Intermediate

x86-64 assembly

x86-64 assembly in Intel syntax: registers and sub-registers, flags, mov and lea, jumps, the stack frame, the System V calling convention and Linux syscalls.

50 cards
Beginner

Regex

Learn regular expressions for good: anchors, classes, greedy vs lazy quantifiers, groups, lookarounds, flags, replacements and the backtracking trap.

50 cards