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; // safeSigned/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 memoryObject 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)