Skip to content
C++

Common C++ Mistakes

Common C++ pitfalls and mistakes — dangling references, iterator invalidation, signed/unsigned comparison, UB, and memory errors.

Dangling References and Pointers

cpp
// Returning reference to local variable
const std::string& bad() {
    std::string s = "hello";
    return s;  // s destroyed — reference dangles
}

// String_view from temporary
std::string_view bad2() {
    return std::string{"hello"};  // temporary destroyed at semicolon
}

// Iterator/pointer after push_back
std::vector<int> v{1, 2, 3};
int* ptr = &v[0];
v.push_back(4);  // reallocation! ptr is now dangling
*ptr = 99;       // UB

// Fix: index, not pointer/iterator
size_t idx = 0;
v.push_back(4);
v[idx] = 99;  // safe

Signed/Unsigned Comparison

cpp
std::vector<int> v{1, 2, 3};
int n = -1;

// Warning / subtle bug: -1 converted to large positive unsigned!
if (n < v.size()) { }   // always true (signed < unsigned → n converted to size_t)
use(v[n]);              // UB: negative index

// Fix: use signed size
if (n < static_cast<int>(v.size())) { }
// Or C++20:
if (n < std::ssize(v)) { }  // std::ssize returns ptrdiff_t

// Loop from size_t to 0
size_t sz = v.size();
for (size_t i = sz - 1; i >= 0; --i)  // INFINITE LOOP: i never < 0 (unsigned)
    use(v[i]);

// Fix:
for (int i = static_cast<int>(sz) - 1; i >= 0; --i)
    use(v[i]);

Iterator Invalidation

cpp
std::vector<int> v{1, 2, 3, 4};

// Erase while iterating — iterator invalidated!
for (auto it = v.begin(); it != v.end(); ++it) {
    if (*it == 2) v.erase(it);  // it is invalid after erase
    // ++it on invalid iterator = UB
}

// Fix: use return value of erase
for (auto it = v.begin(); it != v.end(); ) {
    if (*it == 2) it = v.erase(it);  // erase returns next valid iterator
    else          ++it;
}

// Or C++20: std::erase
std::erase(v, 2);  // removes all 2s cleanly

// Map erasure in range-for: same issue
std::map<int, int> m{{1,1},{2,2},{3,3}};
for (auto& [k, v2] : m)
    if (k == 2) m.erase(k);  // UB: iterator invalidated

// Fix:
for (auto it = m.begin(); it != m.end(); )
    it = (it->first == 2) ? m.erase(it) : std::next(it);

Move Pitfalls

cpp
// Moving then using
std::vector<int> v{1, 2, 3};
auto w = std::move(v);
v.size();  // ok: v is in valid-but-unspecified state
v[0];      // undefined if v is empty after move

// Moving const object — silently copies
const std::string s = "hello";
auto t = std::move(s);  // compiles, but COPIES (can't move const)

// Moving in return — disables NRVO
std::string make_str() {
    std::string s;
    // ... build s ...
    return std::move(s);  // BAD: prevents NRVO, forces move instead of elision
    // Fix: return s;
}

Undefined Behavior (UB)

cpp
// Signed integer overflow
int x = INT_MAX;
x + 1;  // UB (not wrapping!) — use unsigned or check first

// Null pointer dereference
int* p = nullptr;
*p = 5;   // UB

// Out-of-bounds access
int arr[5];
arr[5] = 0;  // UB: out of bounds

// Reading uninitialized variable
int n;
use(n);  // UB: indeterminate value

// Strict aliasing violation
float f = 3.14f;
int* ip = reinterpret_cast<int*>(&f);
*ip;  // UB: accessing float memory as int
// Fix: std::bit_cast or memcpy

// Modifying string literal
char* s = "hello";  // deprecated; should be const char*
s[0] = 'H';         // UB: string literals are in read-only memory

Object Lifetime Errors

cpp
// Use after delete
int* p = new int{42};
delete p;
*p = 99;  // UB

// Double delete
delete p;  // UB again

// Fix: use smart pointers
auto p2 = std::make_unique<int>(42);
// p2.reset(); p2->...; // would throw or UB, but at least not silent

// Stack object outliving reference
struct Wrapper {
    const std::string& ref;
};
Wrapper w{std::string{"temp"}};  // temporary destroyed immediately!
use(w.ref);  // dangling reference

// Fix: store by value
struct WrapperGood {
    std::string value;
};

Implicit Conversion Bugs

cpp
// Narrowing
int big = 300;
char c = big;  // 300 doesn't fit — implementation-defined result

// bool to int
bool flag = true;
int n = flag;  // 1 — usually fine

// Pointer to bool
const char* s = "hello";
if (s) { }   // checks non-null — OK
bool b = s;  // true if non-null — OK but surprising

// Integer to enum class — requires explicit cast
enum class Status : uint8_t { Ok, Error };
Status s2 = 1;               // error: no implicit conversion
Status s3 = static_cast<Status>(1);  // ok (Status::Error)

Threading Bugs

cpp
// Data race: two threads read+write without synchronization
int counter = 0;

std::thread t1([&]{ for (int i=0; i<1000; ++i) ++counter; });
std::thread t2([&]{ for (int i=0; i<1000; ++i) ++counter; });
t1.join(); t2.join();
// counter is NOT 2000 — data race, UB!
// Fix: std::atomic<int> counter{0};

// Deadlock: two mutexes locked in opposite order
std::mutex m1, m2;
// Thread A: lock m1 then m2
// Thread B: lock m2 then m1 — deadlock!
// Fix: always lock in same order, or use std::scoped_lock(m1, m2)

// TOCTOU: check then use with race in between
if (!file_exists("config.json")) {
    // another thread creates it here!
    create_file("config.json");  // fails or overwrites
}

std::string Pitfalls

cpp
// Null terminator position
std::string s = "hello";
s[5];           // ok: null terminator (but don't modify!)
s.data()[5];    // same

// string_view from temporary string
auto sv = std::string{"hello"}.substr(1, 3);
// if std::string::substr returns a string, sv is a string — ok
// if sv were string_view: dangling!

// Comparing with ==
std::string a = "hello", b = "hello";
a == b;   // true (content comparison)
&a == &b; // false (address comparison — always)

// C++11 COW invalidation
// (most modern implementations don't COW, but historical code may assume it)
Edit on GitHub