What to Skip (and Why)
The parts of C++ this course intentionally de-emphasized — and what to reach for instead
Why this lesson exists
The C++ standard is a 1,800-page document describing forty years of accumulated decisions. A complete C++ course would teach all of it. A useful C++ course teaches the ~30% you’ll actually write in 2026 and points at the rest with “read about this if it bites you.”
The previous twelve lessons taught the 30%. This lesson is the opinionated list of what’s left, with brief notes on why each topic was de-emphasized and where it still matters.
Deep inheritance hierarchies
For most of C++‘s history, the recommended “OOP design” looked like:
class Shape {
public:
virtual double area() const = 0;
virtual void draw() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape { /* … */ };
class Rectangle : public Shape { /* … */ };
class Square : public Rectangle { /* … */ };
Modern C++ generally avoids this. Value types, free functions,
templates, and std::variant cover the use cases inheritance
was solving, usually better:
- Need to express “one of several alternatives”?
std::variant<Circle, Rectangle, Triangle>+std::visit. Closed sum type, no virtual dispatch, exhaustiveness-checked at compile time. - Need to express “any type with these operations”? Templates + a concept. Open generic, zero-cost.
- Need a polymorphic callable?
std::functionwraps any callable matching a signature. No class hierarchy needed. - Genuinely need runtime dispatch over an open set of types? Then yes, virtual functions. But prefer single-level interfaces (one base class with pure virtuals; concrete implementers; no further inheritance). Deep hierarchies are almost always a sign that inheritance was the wrong tool.
The exception: framework code that requires you to inherit (Qt, some game engines, unit-test frameworks). Use it the framework’s way; don’t fight it.
Exception-based control flow
C++ has exceptions. The standard library throws (std::bad_alloc,
std::out_of_range, std::system_error). Many codebases catch and
rethrow as part of normal control flow.
This course de-emphasized exceptions because:
- Exceptions are expensive on the throw path — typically 1-10 microseconds. Fine for “we ran out of memory and are dying”; not fine for “this lookup missed.”
- They’re banned in many codebases — game engines (Unreal, custom), embedded systems, kernels, parts of Google. If you can’t rely on them, you can’t design around them.
- C++17’s
std::optional<T>and C++23’sstd::expected<T, E>give you typed “this can fail” returns without the exception machinery. They’re often clearer.
When exceptions are the right tool:
- Constructor failure. Constructors can’t return error codes; throwing is the only standard way to abort construction.
- Boundary errors that bubble through many layers without anyone needing to handle them in between. The classic case: a deep parsing error that just needs to escape to the top-level driver.
For everything else, prefer std::expected or std::optional or a
plain status enum.
iostream ceremony
std::cout << x is fine. std::getline(std::cin, line) is fine.
Everything past that — manipulators, formatters via << chains,
boost::format, the locale system — is mostly legacy.
C++20 added std::format (a Python f-string-shaped formatter) and
C++23 added std::print:
#include <print>
std::print("user {} has {} points\n", name, score);
That’s the modern spelling. It’s faster than iostream, type-safe,
locale-aware on demand, and reads like every other modern formatting
library.
iostream is still needed for object overloads of operator<< (the
canonical “make my type printable” extension point) and for files via
std::fstream. The everyday formatting use case is now std::format /
std::print.
new and delete directly
Lesson 05’s point, restated: in modern C++ application code, raw new
and delete should be rare. Reach for std::make_unique,
std::make_shared, containers, or stack allocation. The remaining
legitimate uses:
- Custom allocators (capstone C1).
- C-API bridges with a custom deleter on a
unique_ptr. new[]anddelete[]for arrays you can’t use a vector for. (Even this is rare —std::vectoris almost always right.)
If a code review presents you with a new without a smart-pointer
wrapper, treat it as a defect until proven otherwise.
C-style arrays (mostly)
int arr[10]; exists. It works. You almost never need it. std::array<int, 10> has the same size and layout, plus .size(), .begin()/.end(),
range-for, and won’t decay to a pointer on the slightest excuse.
std::vector<int> is the answer when the size is dynamic.
The exception: C interop. When a C function takes int* arr, size_t n,
pass vec.data(), vec.size(). You don’t need a C-style array
literally.
unsigned for arithmetic
C++ inherits C’s behavior that mixing signed and unsigned ints in a comparison silently converts the signed one. This produces bugs like:
int i = -1;
unsigned u = 1;
if (i < u) { /* never enters — i became 2^32 - 1 */ }
Modern style: use signed integers for arithmetic. Reserve unsigned
for bit-flags and opaque IDs. std::size_t is unsigned by historical
accident; the Bjarne / Sutter / Stroustrup advice is to use int or
std::ptrdiff_t for sizes when you can, and pay the cast tax when
interfacing with the standard library.
This is contentious. Code that lives entirely inside the standard library’s unsigned-size conventions can stay that way; mixed code does benefit from signed sizes. Read your local style guide.
auto everywhere vs. spelled types
Some codebases lean into auto; others ban it outside iterator
declarations. Both work.
The middle path most readable for the next person:
- Use
autowhen the type is “obvious from context” or “long and ugly”:auto it = map.find(key);auto handler = make_handler(...); - Spell the type when the variable’s meaning is the type itself:
int score = 0;is more readable thanauto score = 0;. - Use
auto&/const auto&in range-for over containers of non-trivial elements. - Never spell the type of an iterator — that’s why
autowas added.
Old-style casts
double d = 3.14;
int i = (int)d; // C-style cast — works but unsafe
int j = static_cast<int>(d); // C++ form — only does what you asked
C-style casts can be a static_cast, const_cast, reinterpret_cast,
or a combination. They’re the cast that fails silently. Always reach
for the named C++ casts:
static_cast<T>— well-defined conversions (inttodouble, base pointer to derived pointer).const_cast<T>— strip or addconst. Rare; usually a sign something’s wrong.reinterpret_cast<T>— bit-pattern reinterpretation. Almost always wrong outside very specific systems-code use cases.dynamic_cast<T>— runtime-checked downcast in a virtual hierarchy. Has a runtime cost; use only when you actually need it.
C-style casts in your own code are a yellow flag. Old code may have plenty; modernize where you can.
Threads where async I/O fits
The standard library’s threading is for CPU-bound parallelism. For I/O-bound work — talking to lots of sockets, files, databases — threads scale poorly. Each blocked thread is a stack and a context switch.
C++ doesn’t ship a standard async I/O framework yet. The ecosystem options:
boost::asio— the canonical proactor-pattern async I/O library.io_uring(Linux 5.1+) — newer kernel API, accessed vialiburingor wrapped inseastar,userver, etc.folly::Futurewithfolly::io::async— Facebook’s library, used in big production codebases.- C++23
std::execution— a future standardization (P2300) of a sender/receiver model. Adoption is still early.
Until C++26 ships executors, picking an async story is a per-project decision. Don’t bolt threads onto an I/O problem.
RTTI and typeid
C++ has runtime type information. typeid(x).name() gives you a mangled
string for x’s type. dynamic_cast uses RTTI to do runtime-checked
downcasts.
The cost: every polymorphic class carries an extra pointer (the vtable’s
type info), and dynamic_cast does string comparison or hash lookup at
runtime. Embedded codebases routinely compile with -fno-rtti.
Use sparingly. Most “I want to know the type” situations are better
solved with std::variant (closed alternatives) or a discriminator
field (open alternatives).
goto
Yes, C++ has goto. No, do not use it. The one historically defensible
use — “break out of nested loops” — has lambdas + return as a cleaner
replacement, or you can refactor into a function.
What’s left
A short list of topics this course did not cover that you may need eventually:
- Modules (C++20). The long-promised fix to
#include. Tooling is still catching up; learn when your build system actually supports them. - Coroutines (C++20).
co_await,co_yield,co_return. The language gives you the primitives; you (or a library) have to build the actual coroutine types. Stable enough for production withcppcoroor similar; not yet ergonomic in the standard. - Allocators (the full API). The capstone C1 touches
std::pmr. The full STL allocator protocol is its own week. std::executionand parallel algorithms. The C++17 parallel algorithm overloads (std::execution::par) are widely available. The full sender/receiver model coming in C++26 is bigger.- Reflection (proposed for C++26). Not standardized yet.
- Networking (the never-quite-shipping
std::net). Useasiofor now. - Embedded-specific topics — bare-metal, no-heap, freestanding C++. Different idioms, mostly subtractive (“you can’t do this here”).
Key takeaways
- Prefer values, free functions, and templates over deep inheritance
hierarchies.
std::variantcovers closed sums; concepts cover open generics;std::functioncovers polymorphic callables. std::expectedandstd::optionalare the modern alternative to exceptions for “this operation can fail.”std::format/std::printreplaceiostreamceremony for everyday formatting.- Raw
new/deleteis rare. Smart pointers and containers. - C-style casts and C-style arrays are both legacy. Use the named
C++ casts and
std::array/std::vector. - Avoid
unsignedfor arithmetic. Use signed types unless you mean bit-flags or opaque IDs. - Use
autoselectively — where the type is obvious or unnameable. Spell concrete types when the type is the meaning.
What’s next
You’ve reached the end of the linear curriculum. The capstones — C1 through C4 — are where the lessons fuse into projects:
- C1 Custom Allocator — exercise lessons 3-6 + concepts.
- C2 Route Planner (A)* — exercise the standard library, generic graphs via concepts, and smart pointer ownership.
- C3 Process Monitor (htop-lite) — exercise RAII over OS resources, ranges for data shaping, and a small TUI.
- C4 Concurrent Web Crawler — exercise the whole concurrency phase (threads, futures, atomics) end-to-end.
Pick the one that sounds most fun. C2 is the most “data structures”; C3 is the most “systems”; C4 is the most “everything you’ve learned.”