0% found this document useful (0 votes)
60 views6 pages

C++ Common Mistakes A Practical Guide

This guide outlines common mistakes in C++ programming, emphasizing the importance of resource management, proper use of the standard library, and effective error handling. Key topics include the Rule of Three/Five/Zero, RAII, avoiding dangling references, and the significance of writing tests. The document encourages adopting modern C++ practices and leveraging tools for better code quality and maintainability.

Uploaded by

Worksm
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
60 views6 pages

C++ Common Mistakes A Practical Guide

This guide outlines common mistakes in C++ programming, emphasizing the importance of resource management, proper use of the standard library, and effective error handling. Key topics include the Rule of Three/Five/Zero, RAII, avoiding dangling references, and the significance of writing tests. The document encourages adopting modern C++ practices and leveraging tools for better code quality and maintainability.

Uploaded by

Worksm
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 6

C++ Common Mistakes: A Practical 1000-Word Guide

C++ is powerful, expressive, and wonderfully fast—but it also makes it easy to shoot yourself
in the foot. This concise field guide covers common mistakes that trip up beginners and
seasoned developers alike, plus quick fixes and habits that keep your code correct, modern,
and maintainable.

Forgetting the Rule of Three/Five/Zero

If your type manages a resource (memory, file handles, mutexes, sockets), you must define
what copying and moving mean. Historically, the Rule of Three said that if you define a
destructor, you probably also need a copy constructor and copy assignment operator. With
C++11, add move constructor and move assignment (Rule of Five). Even better, prefer the
Rule of Zero: delegate ownership to RAII members like std::string, std::vector,
std::unique_ptr, and let the compiler-generated operations do the right thing. You’ll write
less code, avoid leaks, and make behavior obvious.

Manual new/delete Instead of RAII

Raw new/delete and malloc/free invite leaks and exceptions that skip cleanup. Prefer RAII:
use std::unique_ptr for sole ownership, std::shared_ptr for shared ownership (only when
truly needed), and standard containers to hold objects directly. Factory functions that
return smart pointers simplify lifetime management and prevent early returns from leaking
resources. When you see new, ask: can a smart pointer or container own this instead?

Dangling References and Use-After-Free

A reference or pointer becomes dangling when its target goes out of scope or is deleted.
Typical culprits: returning references to local variables, storing references to temporaries, or
keeping iterators after a vector reallocation. Guard against this with clear ownership
models, by returning values (NRVO and move elision make this cheap), and by avoiding
long-lived iterators into containers that reallocate. Prefer std::string by value over const
char* borrowed from a destroyed buffer.
Copying When You Meant to Move

Accidentally copying large objects can devastate performance. Use std::move to express
transfer of resources into containers or return values. Beware: std::move does not move by
itself; it merely casts to an rvalue, enabling a move if one exists. Also, do not std::move from
variables you still need—moved-from objects must remain valid but may be in unspecified
states. When in doubt, profile; copying small trivially copyable types is fine.

Misusing const and constexpr

Forgetting const spreads bugs: mark methods const when they do not mutate logical state,
prefer const& for input parameters, and make free functions take const& where
appropriate. Misunderstanding constexpr is another trap. constexpr means the expression
can be evaluated at compile time when given constant arguments. Use it for compile-time
computations, array sizes, and lookup tables, but remember not everything needs
constexpr—readability and compile times matter.

Uninitialized Variables and Undefined Behavior

C++ does not default-initialize primitive local variables. Reading an uninitialized int or
pointer is undefined behavior. Always initialize variables upon declaration. Prefer
constructors that set members explicitly, and consider in-class member initializers. Enable
warnings like -Wall -Wextra -Wuninitialized and treat warnings as errors in CI to catch these
early. Tools like static analyzers are excellent at spotting suspicious reads.

Signed/Unsigned Mix-ups

Mixing signed and unsigned integers can cause surprising underflow or wraparound,
especially in loops and comparisons with size_t. Consider using std::ssize for signed sizes
(C++20), cast carefully at boundaries, and avoid blind arithmetic between different
domains. Sanity-check loops that count down with unsigned types—they can wrap to huge
values. Static analyzers and sanitizers can catch suspicious conversions.
Range Errors: Off-by-One and Invalid Indices

Classic off-by-one errors appear in loops and slicing operations. Prefer range-based for
loops for iteration, and use at() when you want bounds checking on standard containers.
Treat indices as a separate type or use span-like views to make intent clear. Validate
external inputs before indexing, and write tests that hit boundaries: empty, size 1, and
maximum expected sizes.

Exception Safety and Resource Leaks

Throwing exceptions across code that manually manages resources can leak or double-free.
Design functions with the basic/strong exception safety guarantees and rely on RAII to
unwind cleanly. Prefer nothrow moves for strong exception safety in containers. If you use
exceptions, make destructors noexcept and avoid throwing from them. If a function cannot
complete, either roll back state or leave invariants intact.

C-Style Arrays and Pointer Arithmetic

Hand-rolled arrays and pointer math are fragile. Use std::array for fixed-size buffers and
std::vector for dynamic storage. Prefer span-like views (std::span) for non-owning access.
Algorithms from <algorithm> (e.g., std::copy, std::transform) are safer than manual loops
and let the compiler optimize. Avoid variable-length arrays; they’re non-standard and
hinder portability.

Ignoring the Standard Library Algorithms

Reinventing the wheel yields bugs. Before writing a loop, scan <algorithm> for an
equivalent: std::find_if, std::accumulate, std::sort, std::unique, std::partition. Algorithms
express intent, compose well with ranges (since C++20), and invite optimizations. Prefer
declarative code: say “partition by predicate” instead of hand-coding nested loops.

Incorrect Equality and Hash for Custom Types

If you put a custom type into unordered_map or unordered_set, your hash must be
consistent with operator==. Breaking this contract causes subtle lookup failures. Also
consider providing operator< for ordered containers and stable sorting. When fields change,
re-evaluate what equality and ordering should mean, and test with duplicates and
near-duplicates.

Not Marking Overrides and Missing virtual Destructors

Always use override on virtual functions to catch mismatches at compile time. For base
classes meant for polymorphic deletion, declare a virtual destructor (even defaulted). If a
class is not intended to be inherited, mark it final; this can enable devirtualization and
convey intent. Avoid slicing by passing polymorphic objects by pointer or reference, not by
value.

Mismanaging Concurrency

Data races are undefined behavior. Protect shared state with std::mutex and
std::lock_guard, prefer std::scoped_lock for multiple locks, and avoid holding locks across
blocking operations or callbacks. Use std::condition_variable for coordination and
std::atomic<T> for lock-free counters. Beware false sharing; align hot data or separate
contended fields. Test with ThreadSanitizer and structure code so ownership and lifetimes
are explicit.

Undefined Behavior Landmines

Shifting by or beyond the width of a type, dereferencing null, signed overflow, violating
strict aliasing, and out-of-bounds access are examples of undefined behavior that
optimizers can exploit. Enable UBSan/ASan in development to surface these early and write
tests that stress boundary cases. Prefer safer casts and avoid assumptions about object
layout unless you use standard facilities like std::bit_cast (C++20).

Inefficient Parameter Passing

Passing large objects by value forces copies. Prefer const& for read-only inputs and && for
sink parameters when you intend to move. For small trivially copyable types (ints, pointers,
small structs), passing by value is fine and sometimes faster. Avoid const T& for tiny types; it
can inhibit optimizations and add indirection.

Header Hygiene and ODR Pitfalls

Putting non-inline function definitions or variable definitions in headers can cause One
Definition Rule violations. Prefer inline for header-only helpers, use #pragma once or
include guards, and minimize includes with forward declarations where practical. Use
modules (C++20) where available to speed builds and reduce macro collisions. Keep
headers focused: declare interfaces, implement in source files.

Macro Misuse

Macros ignore scopes and types, leading to baffling bugs. Prefer constexpr variables, inline
functions, and templates. If you must use a macro, wrap it safely, parenthesize parameters,
and avoid name collisions. Never rely on macros for debugging state that crosses
translation units—use logging utilities instead.

Poor Error Handling and Mixed Mechanisms

Mixing exceptions, error codes, and optional return values can confuse callers. Pick a
dominant strategy for a codebase. Use std::expected (where available) or std::optional for
recoverable errors, exceptions for truly exceptional conditions, and document the contract
clearly. Make error paths as visible and testable as the happy path.

Ignoring Tooling: Warnings, Static Analysis, Sanitizers

Compilers are excellent reviewers. Turn on warnings (-Wall -Wextra -Wconversion), make
them fatal in CI, and run clang-tidy regularly. Use AddressSanitizer,
UndefinedBehaviorSanitizer, and ThreadSanitizer in tests. These tools catch the majority of
bugs before they ship. Combine with code formatting (clang-format) to reduce
bikeshedding and keep diffs clean.

Premature Optimization and Micro-benchmarks


Hand-tuned code that guesses at performance often underperforms. First write clear code,
then profile with realistic workloads. Use compiler optimization reports, measure cache
misses, and prefer algorithmic wins over clever tricks. Beware micro-benchmarks that
don’t reflect real data distributions or pipeline effects.

Not Writing Tests

C++ code often integrates with hardware, timing, and legacy systems. A lightweight test
suite using a framework like GoogleTest or Catch2 pays for itself. Write unit tests for
value-semantics types, property tests for algorithms, and fuzzers for parsers and
boundary-heavy components. Tests make refactoring safe and prevent regressions.

Version and Standard Mismatch

Using outdated language features keeps bugs alive. Adopt modern C++ (at least C++17,
ideally C++20 or newer where supported). Prefer ranges, string_view, filesystem,
optional/variant, and smart pointers over legacy patterns. Read release notes for your
compiler and standard library; small updates can unlock better diagnostics and
performance.

Conclusion

C++ rewards discipline. Lean on RAII, favor value semantics, use the standard library
aggressively, and let tools keep you honest. Write tests, enable sanitizers, and prefer clarity
over cleverness. Most mistakes are habits; change the habit and you erase entire classes of
bugs.

You might also like