AlgoMaster Logo

Avoiding Common Pitfalls

Medium Priority32 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

C++ has sharp edges. Some of them are tradition (signed overflow), some are inherited from C (narrowing conversions), and some come from the cost of being a multi-paradigm language (slicing, virtual calls in constructors). This chapter is a catalog of the traps that bite C++ developers most often, with a quick diagnosis and fix for each. It is not deep coverage of any single topic, those live in their own chapters, this is the "things that bite" list you keep next to your keyboard.

Numeric Pitfalls

Numbers in C++ look the same as numbers anywhere else, . The language inherits a small zoo of corner cases from C, and the compiler will accept code that doesn't mean what you think it means.

Signed Integer Overflow Is Undefined Behavior

Signed overflow is undefined behavior. Unsigned overflow wraps around modulo 2^N. They look identical until the optimizer treats one of them as impossible.

What's wrong?

The compiler is allowed to assume orderId + 1 > orderId is always true and optimize accordingly. The runtime result is anyone's guess.

Fix: Check before adding, or use a wider type, or rely on unsigned wrap when wraparound is what you actually want.

Signed/Unsigned Comparison

Comparing a signed value to an unsigned value converts the signed one to unsigned. Negatives become huge positives.

What's wrong?

cartQuantities.size() returns std::size_t (unsigned). discount is converted to a very large unsigned number, so the condition is false and the discount is skipped. Most compilers warn with -Wsign-compare.

Fix: Keep types consistent, or use std::cmp_less from <utility> (C++20) when you must mix.

Implicit Narrowing Conversions

A double will convert to an int, dropping the fractional part. Brace initialization rejects this at compile time.

What's wrong?

The truncation is silent. With = initialization, the compiler shrugs and moves on.

Fix: Use brace initialization {} to make narrowing a compile error, or be explicit with static_cast and a rounding function.

Floating-Point Equality

Floating-point numbers are stored in binary. 0.1 + 0.2 is not exactly 0.3 in IEEE-754. Comparing floats with == will fail in ways that look random.

What's wrong?

The result is 0.30000000000000004, so the equality check fails.

Fix: Compare with a tolerance (epsilon). For currency, use integer cents instead of floating-point dollars.

Object Lifetime Pitfalls

Memory in C++ doesn't clean up after you, and most lifetime bugs hide behind code that looks fine until something on the stack moves an inch.

Iterator Invalidation

When a std::vector grows past its capacity, it reallocates. Every iterator, pointer, and reference into the old buffer points to freed memory. erase invalidates iterators at and after the erased position. Modifying a container mid-iteration is one of the most common bugs in C++ code.

What's wrong?

push_back can reallocate. After that, it points into the old buffer and ++it reads freed memory.

Fix: Don't mutate the container while iterating, or use index-based iteration when you must.

For erase, use the return value, which is the iterator to the next valid element.

Range-Based For Over an Rvalue Temporary Expression

When the range expression itself is a temporary, it lives until the end of the full expression. But if the range comes from a temporary's member function, that subexpression dies first.

What's wrong?

The temporary Cart is destroyed before the loop starts in pre-C++23. getItems() returns a reference to its internal vector, which is now gone. C++23 fixed this for range-based for, but pre-C++23 it is undefined behavior.

Fix: Bind the temporary to a named variable first.

Returning a Reference or Pointer to a Local

Local variables die when the function returns. Returning a pointer or reference to one is a dangling reference, period.

What's wrong?

The compiler may warn, but it will still produce a binary. The caller reads garbage or sees the name briefly before the slot is reused.

Fix: Return by value. Modern C++ uses copy elision and move semantics to make this nearly free.

Dangling Reference via auto& to an Expiring Temporary

auto& binds to lvalues. If the right-hand side is a temporary returned by value, the compiler picks const auto& to extend its lifetime. That extension only applies to the immediate temporary, not to a subobject reached through it.

What's wrong?

placeOrder() returns a temporary Order. Binding const auto& to .customerName does not extend the parent's lifetime, only the member's would-be lifetime, and the parent dies at the end of the full expression.

Fix: Copy the value out, or bind the whole object first.

Returning small objects by value is essentially free thanks to NRVO and move. Don't bend over backwards to return references when value is the simpler, safer choice.

Polymorphism Pitfalls

The pitfalls in this group all stem from one fact: a Product& and a Product are different things at runtime. Forget this and you get silent slicing, dead virtual calls, and leaked subobjects.

Object Slicing on Pass-By-Value

Passing a derived object by value to a function expecting the base type slices off the derived part. The base subobject is copied, the derived fields and vtable pointer are gone.

What's wrong?

The DiscountedProduct is copied into a Product parameter, so only the Product subobject survives. The virtual call dispatches to Product::price.

Fix: Take polymorphic types by reference or pointer.

Calling Virtual Functions from Constructors and Destructors

During base construction, the derived part doesn't exist yet. The vtable points to the base. Same on the way out during destruction. Virtual dispatch in constructors and destructors resolves to the current class, not the most-derived one.

What's wrong?

Output is Order created, not Priority order created. The virtual mechanism is technically active, but the dynamic type during the base constructor is Order.

Fix: Don't rely on virtual dispatch in constructors and destructors. Do the polymorphic work in a separate init() step after construction, or in a factory function.

Forgetting a Virtual Destructor

Deleting a derived object through a base pointer without a virtual destructor is undefined behavior. The derived destructor never runs, resources leak.

What's wrong?

The 1 MB buffer leaks. The behavior is undefined, so the compiler may also do something weirder.

Fix: Mark the base destructor virtual whenever the class is intended to be used polymorphically.

Move Semantics Pitfalls

Move semantics is mostly invisible when it works, but the failure modes are also invisible. The two most common are moving from const (which copies) and self-move (which leaves the object in a wrecked state).

std::move on a const Object

std::move casts to an rvalue reference. If the source is const, the move constructor can't bind (its parameter is non-const rvalue reference), so the copy constructor is called instead. You get a copy when you asked for a move, with no warning.

What's wrong?

The intent was a move, but const rvalue can't bind to Cart(Cart&&). The compiler falls back to Cart(const Cart&). The performance you expected from move is gone.

Fix: Don't move from const. If you really want to move, the source can't be const.

Self-Move

Moving an object into itself can leave it in a destroyed-but-not-overwritten state, depending on how the move assignment is implemented.

What's wrong?

The standard guarantees that std::string is in a valid-but-unspecified state after self-move. It might be empty, it might still have the value, but you can't rely on either.

Fix: Check for self-assignment in your own move assignment operators, and don't write code that self-moves.

Using a Moved-From Object as If It Still Had Its Value

A moved-from object is valid but unspecified. You can assign to it and destroy it, but reading its old value is a mistake.

What's wrong?

customer is in an unspecified state after the move. Some compilers leave it empty, some leave the data intact, none of it is portable.

Fix: After std::move, treat the source as gone. Reassign before reading.

Lambda and auto Pitfalls

Both auto and lambdas trade a bit of explicitness for a lot of convenience. The price you pay is in lifetime and type deduction surprises.

Capturing by Reference in a Lambda That Outlives the Captured Object

A lambda that captures by reference holds raw references into the surrounding scope. If the lambda outlives that scope, the references dangle.

What's wrong?

count is a local in makeCounter. When the function returns, count is gone. The returned lambda's reference dangles.

Fix: Capture by value, or move the state into the lambda explicitly.

auto Drops References and const

Plain auto deduces the value type. A reference becomes a value, a const reference becomes a non-const value. If you actually wanted a reference, you need auto& (or const auto&).

What's wrong?

Each iteration copies an element into s. The increment touches the copy, not the vector. Output is 5 10 15.

Fix: Use auto& when you want to modify the element, const auto& when you want to read without copying.

auto in a range-based for copies the element. For cheap types like int that's fine. For std::string or large structs, prefer const auto& to avoid an allocation per iteration.

auto With Proxy Types

Some containers return proxy types, not real references. std::vector<bool> is the famous one. auto deduces the proxy, and the behavior surprises everyone.

What's wrong?

The proxy points back into the vector. Mutating the vector changes what first refers to. The result depends on the implementation.

Fix: Be explicit when you want a real bool.

Header and Compilation Pitfalls

Headers are macros and source code, glued together by the preprocessor. They are not modules, not packages, not namespaces. Treat them with respect.

using namespace std; in a Header

A header gets #included into every file that needs it. A using namespace std; at the top of a header forces every consumer to pull all of std into the global namespace. Name collisions become inevitable in any non-trivial project.

What's wrong?

Any file including cart.h now sees all of std. Name collisions across libraries are a matter of time.

Fix: Never put using namespace in a header. Qualify names with std::, or use a narrow using std::vector; inside a function.

0 and NULL Instead of nullptr

NULL is 0 in C and often 0 in C++ too. It is an integer, not a pointer, so it can resolve to the wrong overload.

What's wrong?

NULL expands to 0, which matches int better than const char*. The wrong overload runs.

Fix: Use nullptr. It has its own type (std::nullptr_t), and it converts to any pointer type but not to integers.

memcpy Over a Non-Trivially-Copyable Type

memcpy copies raw bytes. If a type has user-defined copy semantics, virtual functions, or non-trivial members, copying bytes bypasses constructors, vtable setup, and reference counts. Result is undefined behavior, even if the program seems to work.

What's wrong?

std::string holds a heap pointer. The byte copy duplicates that pointer. Both objects destruct, both call free on the same buffer.

Fix: Use the copy constructor or assignment operator. memcpy is only safe for trivially copyable types (std::is_trivially_copyable_v<T> is true).

Forgetting an #include

A header you use might be pulled in transitively through another header today, and not tomorrow. The build breaks when the transitive path changes.

What's wrong?

This compiles on some standard library implementations because <string> includes <vector> indirectly. Switch compilers or standard library versions and the code stops compiling.

Fix: Include every header that defines a name you use. Don't rely on transitive includes. Tools like include-what-you-use (IWYU) can audit this automatically.

Order of Evaluation Surprises

In f(g(), h()), the order in which g() and h() are evaluated is unspecified. C++17 sequenced the evaluation of function arguments such that each argument is fully evaluated before the next one starts, but the order between them is still unspecified. Side effects can run in either order.

What's wrong?

Don't rely on side effects between arguments. Same problem with i = i++ + ++i; and friends.

Fix: Sequence the calls explicitly.

Quiz

Avoiding Common Pitfalls Quiz

9 quizzes