CppBest Practices

Best Practices

C++ gives you enormous control, and with it, plenty of ways to shoot yourself in the foot. Most of the advice below has come up individually on earlier pages — this page pulls it together into one practical checklist you can hold yourself to on every project.

The checklist
  1. Prefer brace initialization {} for variables — int x{5}; catches narrowing conversions at compile time that int x = 5.7; would silently truncate.

  2. Use auto where it aids readability (long iterator types, obvious right-hand sides) — but not where it hides a type that matters for understanding the code.

  3. Prefer references and smart pointers over raw pointers for ownership and access — reserve raw pointers for non-owning, optional, "observe only" cases.

  4. Always initialize variables at the point of declaration. An uninitialized local variable holds garbage, not zero — reading it is undefined behavior.

  5. Use RAII for every resource — memory, file handles, mutexes, sockets — so cleanup is automatic and exception-safe.

  6. Prefer std::vector and other STL containers over raw C arrays — they manage their own memory, know their own size, and work with the STL algorithms.

  7. Catch exceptions by const referencecatch (const std::exception& e) — never by value, to avoid slicing derived exception types.

  8. Mark every overriding virtual function with override — the compiler will then catch signature mismatches that would otherwise silently create a new, unrelated virtual function.

  9. Prefer const wherever a value or reference is not meant to change — it documents intent and lets the compiler catch accidental mutation.

  10. Compile with -Wall -Wextra (and treat warnings seriously) — many undefined-behavior bugs and typos surface as warnings long before they become runtime crashes.

  11. Follow the Rule of Zero when possible (let the compiler generate special member functions by relying on RAII members), and the Rule of Three/Five when a class manages a resource directly — define destructor, copy constructor, and copy assignment together (and move constructor/assignment for Rule of Five).

A quick before/after

before-and-after.cpp

CPP
// Before: raw pointer, manual new/delete, no const, no override
class Widget : public Base {
  int* data;
public:
  Widget(int n) { data = new int[n]; }
  ~Widget() { delete[] data; }
  void render() { /* ... */ } // meant to override Base::render
};

// After: RAII, smart pointer, const-correct, override-checked
class Widget : public Base {
  std::vector<int> data;
public:
  explicit Widget(std::size_t n) : data(n) {}
  void render() const override { /* ... */ }
};
The "after" version needs no hand-written destructor at all — std::vector manages its own memory via RAII, so the compiler-generated destructor is correct automatically (the Rule of Zero in action). Marking render() with override also means the compiler will now flag it as an error if Base::render() doesn't actually exist with a matching signature, instead of silently compiling a bug.