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_ptrinstead of rawnew/deletestd::shared_ptrfor shared ownership, not manual reference countingstd::make_unique/std::make_sharedpreferred overnew- No raw
delete(smart pointers handle it) std::unique_ptrwith custom deleters for RAII- References or
std::optionalinstead of nullable raw pointers - No manual memory management in constructors (use RAII)
- Arrays use
std::vectororstd::array, not raw arrays
Object Lifecycle
- Rule of Five followed when needed (destructor, copy/move constructors, copy/move assignment)
- Default operations
= defaultwhen possible - Deleted operations
= deletewhen 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::moveused for transferring ownership- Functions return by value (RVO/NRVO optimizes)
- Move-only types used for unique resources
- Perfect forwarding with
std::forwardin templates - R-value references
&&for move operations only - No moving from
constobjects (defeats purpose) - Moved-from objects in valid but unspecified state
STL Containers & Algorithms
std::vectordefault container choicereserve()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_backused instead ofpush_backfor efficiency
Exception Safety
- RAII ensures cleanup on exceptions
- Functions marked
noexceptwhen 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
noexceptfor efficiency
Modern C++ Features
autofor complex types:auto it = container.begin()- Structured bindings:
auto [key, value] = map.insert(...) nullptrinstead ofNULLor0- Range-based for when appropriate
- Lambda expressions for callbacks and algorithms
constexprfor compile-time evaluationstd::optionalfor 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.

