C++ Code Review Checklist: Modern C++ and Memory Safety

Published on
Written byChristoffer Artmann
C++ Code Review Checklist: Modern C++ and Memory Safety

Raw new and delete create memory leaks and exception safety problems. Manual resource management causes use-after-free bugs. Unnecessary copies tank performance. Missing move semantics forces expensive object copying. These issues compile successfully but cause memory leaks, crashes, or poor performance under load.

This checklist helps catch what matters: raw pointer usage, missing RAII, inefficient object handling, and code that ignores modern C++ features like smart pointers, move semantics, and STL algorithms.

Quick Reference Checklist

Use this checklist as a quick reference during code review.

Modern Memory Management

  • std::unique_ptr instead of raw new/delete
  • std::shared_ptr for shared ownership, not manual reference counting
  • std::make_unique/std::make_shared preferred over new
  • No raw delete (smart pointers handle it)
  • std::unique_ptr with custom deleters for RAII
  • References or std::optional instead of nullable raw pointers
  • No manual memory management in constructors (use RAII)
  • Arrays use std::vector or std::array, not raw arrays

Object Lifecycle

  • Rule of Five followed when needed (destructor, copy/move constructors, copy/move assignment)
  • Default operations = default when possible
  • Deleted operations = delete when copying/moving shouldn't occur
  • Move constructors/assignment marked noexcept
  • Copy operations properly handle resources
  • Self-assignment handled in copy assignment
  • Destructors release all resources
  • Constructors use member initializer lists

Move Semantics

  • std::move used for transferring ownership
  • Functions return by value (RVO/NRVO optimizes)
  • Move-only types used for unique resources
  • Perfect forwarding with std::forward in templates
  • R-value references && for move operations only
  • No moving from const objects (defeats purpose)
  • Moved-from objects in valid but unspecified state

STL Containers & Algorithms

  • std::vector default container choice
  • reserve() called when size known
  • Algorithms preferred over handwritten loops
  • <algorithm> functions used: std::find, std::sort, std::transform
  • Range-based for loops: for (const auto& item : container)
  • Iterators not invalidated by container modifications
  • Containers not indexed beyond bounds
  • emplace_back used instead of push_back for efficiency

Exception Safety

  • RAII ensures cleanup on exceptions
  • Functions marked noexcept when they don't throw
  • Constructors don't leak on exceptions
  • Strong exception guarantee where possible
  • No exceptions from destructors
  • Catch by reference: catch (const Exception& e)
  • Resources cleaned up even when exceptions occur
  • Move operations noexcept for efficiency

Modern C++ Features

  • auto for complex types: auto it = container.begin()
  • Structured bindings: auto [key, value] = map.insert(...)
  • nullptr instead of NULL or 0
  • Range-based for when appropriate
  • Lambda expressions for callbacks and algorithms
  • constexpr for compile-time evaluation
  • std::optional for optional values
  • Attributes: [[nodiscard]], [[maybe_unused]]

Modern Memory Management

std::unique_ptr provides automatic memory management for single ownership. The pattern auto ptr = std::make_unique<T>(args) allocates and wraps the pointer. When unique_ptr goes out of scope, it automatically deletes the object. This eliminates manual delete and prevents leaks even when exceptions occur. When we see raw new in modern C++, unique_ptr usually improves safety.

std::shared_ptr enables shared ownership through reference counting. Multiple shared_ptrs can point to the same object, which is deleted when the last shared_ptr is destroyed. This beats manual reference counting but has overhead, so we use it only when ownership truly is shared. When reviewing shared_ptr usage, we verify shared ownership is necessary rather than unique_ptr with references.

std::make_unique and std::make_shared are preferred over direct new. These functions are exception-safe and (for make_shared) more efficient. The pattern std::make_unique<T>(args) is safer than std::unique_ptr<T>(new T(args)). When we see smart pointers constructed with new, make functions improve the code.

Custom deleters enable RAII for non-memory resources. The pattern std::unique_ptr<FILE, decltype(&fclose)>(fopen(path, "r"), &fclose) manages file handles automatically. When we see manual resource cleanup, unique_ptr with custom deleters provides RAII.

References or std::optional express optional values better than nullable raw pointers. Instead of returning T* that might be null, std::optional<T> makes the optionality explicit in the type system. When reviewing function signatures with raw pointers, we consider whether references or optional better express the contract.

Object Lifecycle and The Rule of Five

The Rule of Five states that if a class needs any of destructor, copy constructor, copy assignment, move constructor, or move assignment, it likely needs all five. These special member functions handle resource management. When we see a custom destructor without defined copy/move operations, the rule of five likely applies.

Default implementations using = default let the compiler generate the function. When the default behavior is correct, = default is clearer than writing it manually. This documents that default semantics are intentional. When we see boilerplate special member functions, default might express the same intent.

Deleted operations using = delete prevent copying or moving when it doesn't make sense. A file handle wrapper shouldn't be copied, so FileHandle(const FileHandle&) = delete; prevents it. This catches mistakes at compile time. When classes manage unique resources, deleting copy operations prevents double-free bugs.

Move operations should be noexcept for performance. Standard containers optimize moves differently when operations are noexcept. The pattern T(T&& other) noexcept enables these optimizations. When reviewing move operations, we verify the noexcept qualifier.

Copy assignment must handle self-assignment. The pattern if (this != &other) checks for self-assignment before releasing resources. Without this check, obj = obj; can delete resources before copying them. When we see copy assignment without self-assignment protection, that's a potential bug.

Member initializer lists initialize members before the constructor body executes. The pattern Constructor() : member1(value1), member2(value2) is more efficient than assignment in the constructor body. When we see assignments in constructors, initializer lists usually perform better.

Move Semantics and Perfect Forwarding

std::move casts to rvalue reference, enabling move operations. The pattern std::vector<int> v2 = std::move(v1); transfers v1's contents to v2 rather than copying. After the move, v1 is in a valid but unspecified state—it can be destroyed or assigned but shouldn't be used otherwise. When we see expensive copies that could be moves, std::move improves performance.

Functions returning by value enable copy elision (RVO/NRVO). The pattern return value; where value is a local variable allows the compiler to construct directly in the return location. Manual optimization with output parameters or pointers prevents this. When we see output parameters for returns, returning by value often performs as well or better.

Move-only types like std::unique_ptr can't be copied, only moved. This enforces unique ownership at compile time. When designing types that represent unique resources, making them move-only prevents ownership bugs. When we see reference counting on resources that should have unique ownership, move-only types clarify the design.

Perfect forwarding using std::forward preserves value categories in template functions. The pattern template<typename T> void f(T&& arg) { g(std::forward<T>(arg)); } forwards arg to g with the same value category (lvalue/rvalue) it received. This matters for template functions that forward to other functions.

STL Containers and Algorithms

std::vector should be the default container choice. It provides good performance for most use cases through contiguous storage and cache locality. Specialized containers like list, deque, or unordered_map solve specific problems, but vector handles general cases well. When reviewing container choices, we verify the selected container matches access patterns.

reserve() pre-allocates capacity when size is known, avoiding reallocations. The pattern vec.reserve(expected_size); before a loop that fills the vector improves performance. When we see vectors growing in loops without reserve, pre-allocation helps.

STL algorithms express intent better than handwritten loops. Instead of manual loops, std::sort(vec.begin(), vec.end()) or std::find_if(vec.begin(), vec.end(), predicate) communicate purpose clearly. When we see loops implementing standard algorithms, STL versions are often clearer and sometimes faster.

Range-based for loops simplify iteration. The pattern for (const auto& item : container) iterates without manual iterator management. When loop bodies just access elements sequentially, range-based for improves readability. We verify the reference type matches intent—const auto& for reading, auto& for modifying.

Iterator invalidation causes bugs when containers modify during iteration. Operations like push_back might invalidate iterators if reallocation occurs. When we see iteration combined with modification, we verify iterators remain valid or are refreshed after modifications.

emplace_back constructs elements in place rather than copying/moving. The pattern vec.emplace_back(args...) is more efficient than vec.push_back(T(args...)) because it avoids the temporary. When we see push_back with constructed temporaries, emplace_back reduces overhead.

Exception Safety with RAII

RAII (Resource Acquisition Is Initialization) ensures cleanup even when exceptions occur. Resources acquired in constructors and released in destructors are automatically managed. This pattern eliminates manual cleanup code and prevents leaks. When we see manual resource management, RAII often simplifies the code and improves safety.

noexcept indicates functions won't throw exceptions. This enables optimizations and documents behavior. Move operations should be noexcept when possible because standard containers use different code paths for noexcept moves. When reviewing exception specifications, we verify they're accurate.

Constructors that might throw must not leak resources. If a constructor acquires multiple resources and later initialization fails, early resources must be released. Smart pointers and member RAII objects handle this automatically. When constructors manually manage resources, exceptions can cause leaks.

Strong exception guarantee means operations succeed completely or leave state unchanged. This is difficult to achieve but valuable for critical operations. When functions modify state, we consider what happens when exceptions occur mid-operation. Copy-and-swap idiom provides strong guarantee for assignment.

Destructors must not throw exceptions. A throwing destructor during stack unwinding from another exception calls std::terminate, crashing the program. Destructors should catch and handle their own exceptions. When we see destructors with operations that might throw, exception handling is needed.

Catching exceptions by reference avoids slicing and extra copies. The pattern catch (const Exception& e) catches without copying. Catching by value slices derived exception types to base type, losing information. When we see catch by value, catch by reference preserves exception details.

Bringing It Together

Effective C++ code review balances modern features with practical concerns. Modern C++ provides tools for safe, efficient code through smart pointers, move semantics, and RAII. Code written in old C++ style with manual memory management and raw pointers misses these benefits.

Not all issues require immediate attention. Using push_back instead of emplace_back rarely matters. Missing move constructor for a type with expensive members affects performance significantly. The key is identifying issues that impact safety, correctness, or performance rather than stylistic preferences.

C++ continues evolving with recent standards adding ranges, concepts, and coroutines. Teams benefit from adopting modern features that improve safety and clarity. Code review creates opportunities to share knowledge about modern C++ capabilities and discuss when new features solve real problems effectively.