Introduction: When C++ Ceases to Be "Just a Language"
You have already written a dozen console programs, learned to use std::vector, and even figured out templates. But C++ is a language that never forgives a superficial approach. A beginner developer transitions to a new level exactly at the moment when they encounter undefined behavior (UB) and begin to understand that the compiler is not a kind friend, but a strict counterparty.
In this article, we will not analyze trivial errors like a forgotten ; or confusion between = and ==. We will talk about the pitfalls that await those who already know the syntax but have not yet mastered the philosophy of the language. These errors are the cause of 90% of hard-to-find bugs in industrial code.
1. Ignoring the Rule of Five and Rule of Zero
Problem: Implicitly Generated Special Member Functions
Many developers transitioning from Java or Python believe that the compiler will "do everything nicely" with copying and moving objects. This is a dangerous misconception.
Consider a class with a dynamic resource:
class Buffer {public: Buffer(size_t size) : data(new char[size]), size(size) {} ~Buffer() { delete[] data; } // Everything is fine? No! No copy constructor or copy assignment operator!private: char* data; size_t size;};
void process() { Buffer a(100); Buffer b = a; // Shallow copy! Two pointers to the same memory.} // Double free — UB!The Rule of Five states: if you define at least one of the five special methods (destructor, copy constructor, copy assignment operator, move constructor, move assignment operator), you must define them all.
Solution: Use std::unique_ptr or std::vector for resource management. This is the Rule of Zero approach: if your class does not manage resources directly, the compiler will generate correct special functions automatically.
class Buffer {public: Buffer(size_t size) : data(std::make_unique<char[]>(size)), size(size) {} // Rule of Zero — destructor, copy, and move work correctly!private: std::unique_ptr<char[]> data; size_t size;};2. Incorrect Use of Move Semantics
Problem: Move Does Not Automatically Make the Object "Empty"
One of the most insidious errors is believing that after std::move, the source object becomes "safe" to use. In reality, std::move is simply a cast to an rvalue reference. The state of the moved-from object depends on the implementation.
std::string a = "Hello, World!";std::string b = std::move(a);std::cout << a; // Technically — UB? No, but the content of a is undefined!// Could be an empty string, or it could be "Hello, World!"The standard only guarantees that the moved-from object is in a valid but unspecified state. You can call a.clear() or a = "new value", but you should not rely on its content.
Tip: After moving, either reset the object to a known state or stop using it. For user-defined types, always document the postconditions of the move constructor.
3. Memory Leaks via Raw Pointers and Exceptions
Problem: Exceptions "Break" the Resource Release Chain
Even experienced developers sometimes write code with manual memory management:
void process() { int* ptr1 = new int(42); int* ptr2 = new int(100); // If an exception is thrown here — leak of ptr1! dangerous_function(); delete ptr2; delete ptr1;}C++ does not have finally