C++ inherits C's full range of undefined behavior (out-of-bounds access, use-after-free, signed integer overflow, uninitialized reads) since it shares C's low-level memory model, and adds distinctly C++-flavored categories on top. Iterator invalidation is a particularly common one: many container operations (a vector reallocating on push_back, erasing an element from a map) silently invalidate existing iterators, and continuing to use one afterward is undefined behavior with no compiler warning by default. Calling a virtual function from a constructor or destructor is another genuine C++-specific trap: it does NOT dispatch to the most-derived override, since the derived part of the object isn't fully constructed (or has already been destructed) at that point — a subtlety that surprises many people coming from languages without this restriction.
Iterator invalidation: a genuine, common trap
push_back MIGHT trigger a reallocation if the vector's current capacity is exhausted, moving all its elements to a new memory block — it is entirely valid content-wise, but it, first, and every other existing iterator into v are now dangling.
std::vector<int> v = {1, 2, 3};
auto it = v.begin();
v.push_back(4); // MIGHT reallocate — if it does, it is now DANGLING
// std::cout << *it; // UNDEFINED BEHAVIOR if a reallocation happened — compiles fine either way
// safer: re-obtain the iterator after any modification that might invalidate it
it = v.begin();
std::cout << *it; // safeVirtual functions in constructors/destructors don't dispatch as expected
Calling a virtual function inside Base's constructor calls Base's OWN version, never Derived's override — even if the object being constructed is genuinely a Derived — because the Derived portion of the object hasn't been constructed yet at that point, so treating it as a Derived would be unsafe.
class Base {
public:
Base() {
init(); // calls Base::init(), NOT Derived::init() — even for a Derived object!
}
virtual void init() { std::cout << "Base::init\n"; }
};
class Derived : public Base {
public:
void init() override { std::cout << "Derived::init\n"; }
};
Derived d; // prints "Base::init" — surprising, but NOT undefined behavior, just non-obvious dispatchSigned integer overflow: still genuinely undefined
Unlike unsigned overflow (which wraps around predictably, defined by the standard), signed overflow is undefined behavior — the compiler is permitted to assume it never happens, which has led to real, surprising optimizer behavior in code that relied on it silently wrapping the way C's or Java's signed overflow does.
int max = std::numeric_limits<int>::max();
int overflowed = max + 1; // UNDEFINED BEHAVIOR — not a defined wraparound, unlike unsigned