← C++, End to End

C++, End to End

Idiomatic C++ and Common Pitfalls

This closing chapter is less about new syntax and more about how experienced C++ programmers actually write the language day to day, versus how a lot of code from twenty years ago (or a lot of beginner code today) still gets written.

Prefer the standard library over hand-rolled loops

c++ · live, editable, runnableOpen in Compiler Explorer ↗

std::max_element isn't just shorter, it's more honest about intent: a reader sees the name and immediately knows what the code does, instead of having to trace through a hand-written loop to reverse-engineer that it's finding a maximum.

RAII everywhere, manual cleanup nowhere

If you find yourself writing a matched pair of "acquire" and "release" calls anywhere (lock/unlock, open/close, new/delete, begin/end), that pair belongs inside a constructor and destructor, not scattered through your function's control flow where an early return or an exception can skip the cleanup half.

const by default

Declare variables, parameters, and member functions const unless you have a specific reason not to. It's not just documentation, the compiler enforces it: a const method literally cannot accidentally mutate the object, and a const variable literally cannot be accidentally reassigned three functions later by someone who didn't read the whole file.

Avoid raw owning pointers

A raw pointer is fine for observing something you don't own. A raw pointer that's supposed to represent ownership (something only this code is responsible for eventually deleting) is a bug waiting for the first exception, early return, or forgotten delete along whichever code path doesn't get tested. Use std::unique_ptr or std::shared_ptr for ownership, always.

Avoid C-style casts

(int)someDouble looks harmless, but a C-style cast will silently do whatever combination of static_cast, const_cast, and reinterpret_cast it takes to make the code compile, including ones you almost certainly didn't intend, like casting away const. The named casts (static_cast, const_cast, and so on) are more verbose specifically so that each one only does one narrow, explicit thing, and a code reviewer (or you, six months later) can tell exactly what's happening by the cast's name alone.

Prefer std::string and std::string_view over C-strings

std::string owns and manages its own buffer safely; std::string_view is a lightweight, non-owning view into existing string data, useful for function parameters that just need to read a string without caring who owns it and without forcing an unnecessary copy. Raw char* string handling is where the sibling C course's entire "strings and buffer footguns" chapter worth of bugs lives, and C++ gives you tools to avoid nearly all of it.

Initialize everything

c++ · live, editable, runnableOpen in Compiler Explorer ↗

An uninitialized variable in C++ doesn't reliably contain zero. It contains whatever bits happened to already be sitting in that memory, which might look like a plausible value while testing and then be garbage the moment the stack layout shifts slightly. Prefer brace initialization ({}) as a habit; it also refuses narrowing conversions that would silently lose data, which is a nice bonus catch.

Prefer scoped enums

enum class Color { Red, Green, Blue } instead of a plain enum. Scoped enums don't leak their enumerator names into the surrounding scope, don't implicitly convert to int (a real source of bugs when comparing unrelated enums that happen to share numeric values), and force you to write Color::Red instead of a bare Red that could mean almost anything.

If you remember five things from this course

  • RAII is why C++ can be both fast and safe: tie every resource's lifetime to an object's constructor/destructor, and exceptions, early returns, and forgotten cleanup stop being a source of bugs.
  • Prefer value semantics and the standard library's containers/algorithms to hand-rolled pointer manipulation; reach for raw new/delete and manual loops only when you have a specific reason the abstraction doesn't fit.
  • Templates give you compile-time polymorphism (zero runtime cost, but code generated per type); virtual functions give you runtime polymorphism (one shared implementation, small dispatch cost). Know which one your problem actually needs.
  • Move semantics exist so that returning and passing heavy objects by value stopped meaning expensive copies. Let the compiler move for you by default; reach for std::move only when you specifically mean "I'm done with this, take it."
  • Modern C++ (11 through 23) changed the language's center of gravity: if code you're reading looks like it was written in 2003 (raw pointers everywhere, manual new/delete, no auto, no range-for), that's not automatically wrong, but it's very likely leaving safety and readability on the table that the language has offered for over a decade now.