C++ programming: Common Mistakes


๐พ Memory Management & RAII Mistakes
1. Raw Pointers Instead of Smart Pointers (Modern C++)
โ WRONG:
class BadClass {
private:
int* data;
public:
BadClass() : data(new int[100]) {}
~BadClass() {
delete[] data; // What if exception is thrown before this?
}
// Missing copy constructor and assignment operator!
// Will cause double delete or shallow copy
};
void bad_function() {
int* ptr = new int(42);
// Some code that might throw exception
delete ptr; // Might never be reached!
}
โ CORRECT:
#include <memory>
class GoodClass {
private:
std::unique_ptr<int[]> data;
public:
GoodClass() : data(std::make_unique<int[]>(100)) {}
// Rule of Zero: compiler-generated destructor is fine
// Smart pointers handle cleanup automatically
};
void good_function() {
auto ptr = std::make_unique<int>(42);
// Exception-safe: ptr automatically deleted
}
// For shared ownership
std::shared_ptr<int> shared_ptr = std::make_shared<int>(42);
2. Violating Rule of Three/Five/Zero
โ WRONG:
class BadResource {
private:
int* data;
size_t size;
public:
BadResource(size_t n) : size(n), data(new int[n]) {}
~BadResource() { delete[] data; }
// MISSING: Copy constructor, copy assignment, move constructor, move assignment
// This will cause double-delete when objects are copied!
};
void demonstrate_problem() {
BadResource obj1(100);
BadResource obj2 = obj1; // Shallow copy! Both point to same memory
// When obj1 and obj2 are destroyed, double-delete occurs!
}
โ CORRECT:
class GoodResource {
private:
std::unique_ptr<int[]> data;
size_t size;
public:
// Constructor
GoodResource(size_t n) : size(n), data(std::make_unique<int[]>(n)) {}
// Rule of Five (if you need custom behavior)
// Copy constructor
GoodResource(const GoodResource& other)
: size(other.size), data(std::make_unique<int[]>(other.size)) {
std::copy(other.data.get(), other.data.get() + size, data.get());
}
// Copy assignment
GoodResource& operator=(const GoodResource& other) {
if (this != &other) {
size = other.size;
data = std::make_unique<int[]>(size);
std::copy(other.data.get(), other.data.get() + size, data.get());
}
return *this;
}
// Move constructor
GoodResource(GoodResource&& other) noexcept
: size(other.size), data(std::move(other.data)) {
other.size = 0;
}
// Move assignment
GoodResource& operator=(GoodResource&& other) noexcept {
if (this != &other) {
data = std::move(other.data);
size = other.size;
other.size = 0;
}
return *this;
}
// Destructor (automatically handled by unique_ptr)
~GoodResource() = default;
};
// Better: Rule of Zero - use standard containers
class BestResource {
private:
std::vector<int> data;
public:
BestResource(size_t n) : data(n) {}
// All special members automatically generated correctly!
};
3. Memory Leaks with Exceptions
โ WRONG:
void bad_function() {
int* ptr1 = new int(10);
int* ptr2 = new int(20); // If this throws, ptr1 leaks!
// Some code that might throw
process_data(*ptr1, *ptr2);
delete ptr1;
delete ptr2;
}
class BadClass {
int* ptr1;
int* ptr2;
public:
BadClass() {
ptr1 = new int(10);
ptr2 = new int(20); // If this throws, ptr1 leaks!
}
};
โ CORRECT:
void good_function() {
auto ptr1 = std::make_unique<int>(10);
auto ptr2 = std::make_unique<int>(20); // Exception safe
process_data(*ptr1, *ptr2);
// Automatic cleanup, no explicit delete needed
}
class GoodClass {
std::unique_ptr<int> ptr1;
std::unique_ptr<int> ptr2;
public:
GoodClass()
: ptr1(std::make_unique<int>(10))
, ptr2(std::make_unique<int>(20)) {
// Exception safe: if ptr2 throws, ptr1 is already constructed
// and will be automatically destroyed
}
};
๐ง Object-Oriented Programming Pitfalls
4. Slicing Problem
โ WRONG:
class Base {
public:
virtual void print() { std::cout << "Base\n"; }
int base_data = 1;
};
class Derived : public Base {
public:
void print() override { std::cout << "Derived\n"; }
int derived_data = 2;
};
void bad_function() {
Derived d;
Base b = d; // SLICING! derived_data is lost
b.print(); // Calls Base::print(), not Derived::print()
std::vector<Base> vec;
vec.push_back(Derived{}); // SLICING! Objects are sliced when stored
}
โ CORRECT:
void good_function() {
Derived d;
Base& b = d; // Reference, no slicing
b.print(); // Calls Derived::print()
Base* ptr = &d; // Pointer, no slicing
ptr->print(); // Calls Derived::print()
// Use polymorphic containers
std::vector<std::unique_ptr<Base>> vec;
vec.push_back(std::make_unique<Derived>());
vec[0]->print(); // Calls Derived::print()
}
5. Missing Virtual Destructor
โ WRONG:
class Base {
public:
~Base() { std::cout << "Base destructor\n"; }
// Non-virtual destructor!
};
class Derived : public Base {
private:
int* data;
public:
Derived() : data(new int[100]) {}
~Derived() {
delete[] data;
std::cout << "Derived destructor\n";
}
};
void demonstrate_problem() {
Base* ptr = new Derived();
delete ptr; // UNDEFINED BEHAVIOR! Only Base destructor called
// Derived destructor never called -> memory leak!
}
โ CORRECT:
class Base {
public:
virtual ~Base() { std::cout << "Base destructor\n"; }
// Virtual destructor ensures proper cleanup
};
class Derived : public Base {
private:
std::unique_ptr<int[]> data; // Better: use smart pointers
public:
Derived() : data(std::make_unique<int[]>(100)) {}
~Derived() override {
std::cout << "Derived destructor\n";
// data automatically cleaned up
}
};
void good_function() {
std::unique_ptr<Base> ptr = std::make_unique<Derived>();
// Correct destructor sequence guaranteed
}
6. Improper Operator Overloading
โ WRONG:
class BadNumber {
int value;
public:
BadNumber(int v) : value(v) {}
// Wrong return type
void operator+(const BadNumber& other) {
value += other.value; // Modifies this object!
}
// Missing const
bool operator==(BadNumber& other) {
return value == other.value;
}
// Assignment returns void
void operator=(const BadNumber& other) {
value = other.value;
}
};
โ CORRECT:
class GoodNumber {
int value;
public:
GoodNumber(int v) : value(v) {}
// Binary operators should return new object
GoodNumber operator+(const GoodNumber& other) const {
return GoodNumber(value + other.value);
}
// Comparison operators should be const
bool operator==(const GoodNumber& other) const {
return value == other.value;
}
// Assignment should return reference for chaining
GoodNumber& operator=(const GoodNumber& other) {
if (this != &other) {
value = other.value;
}
return *this;
}
// Compound assignment operators
GoodNumber& operator+=(const GoodNumber& other) {
value += other.value;
return *this;
}
};
๐งต Template & Generic Programming Mistakes
7. Template Instantiation Issues
โ WRONG:
template<typename T>
class BadContainer {
private:
T* data;
size_t size;
public:
BadContainer(size_t n) : size(n) {
data = new T[n]; // What if T constructor throws?
}
~BadContainer() {
delete[] data; // Might leak if exception in constructor
}
T& get(size_t index) {
return data[index]; // No bounds checking!
}
// Missing template specialization considerations
void print() {
for (size_t i = 0; i < size; ++i) {
std::cout << data[i] << " "; // What if T doesn't support <<
}
}
};
โ CORRECT:
template<typename T>
class GoodContainer {
private:
std::vector<T> data;
public:
explicit GoodContainer(size_t n) : data(n) {}
T& get(size_t index) {
if (index >= data.size()) {
throw std::out_of_range("Index out of bounds");
}
return data[index];
}
const T& get(size_t index) const {
if (index >= data.size()) {
throw std::out_of_range("Index out of bounds");
}
return data[index];
}
template<typename U = T>
auto print() -> decltype(std::cout << std::declval<U>(), void()) {
for (const auto& item : data) {
std::cout << item << " ";
}
}
};
// SFINAE (Substitution Failure Is Not An Error) for better error messages
template<typename T>
class SafeContainer {
static_assert(std::is_copy_constructible_v<T>,
"T must be copy constructible");
static_assert(std::is_destructible_v<T>,
"T must be destructible");
// Implementation...
};
8. Template Argument Deduction Problems
โ WRONG:
template<typename T>
void bad_function(T value) {
// Problem: always copies, even for large objects
}
template<typename T>
class BadWrapper {
T data;
public:
BadWrapper(T value) : data(value) {} // Always copies!
};
void demonstrate_problems() {
std::vector<int> large_vector(1000000);
bad_function(large_vector); // Expensive copy!
BadWrapper<std::vector<int>> wrapper(large_vector); // Another copy!
}
โ CORRECT:
// Perfect forwarding
template<typename T>
void good_function(T&& value) {
// Use std::forward to preserve value category
internal_function(std::forward<T>(value));
}
template<typename T>
class GoodWrapper {
T data;
public:
template<typename U>
GoodWrapper(U&& value) : data(std::forward<U>(value)) {}
// Perfect forwarding in constructor
};
// Alternative: explicit overloads
template<typename T>
void alternative_function(const T& value) { /* for lvalues */ }
template<typename T>
void alternative_function(T&& value) { /* for rvalues */ }
void demonstrate_solutions() {
std::vector<int> large_vector(1000000);
good_function(large_vector); // No copy, passes by reference
good_function(std::move(large_vector)); // Move semantics
GoodWrapper wrapper(std::move(large_vector)); // Perfect forwarding
}
๐ Iterator & Container Mistakes
9. Iterator Invalidation
โ WRONG:
void bad_vector_operations() {
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (*it % 2 == 0) {
vec.erase(it); // UNDEFINED BEHAVIOR! Iterator invalidated
}
}
// Another problem
auto it = vec.begin();
vec.push_back(100); // May invalidate all iterators!
*it = 42; // UNDEFINED BEHAVIOR!
}
void bad_map_operations() {
std::map<int, std::string> m = {{1, "one"}, {2, "two"}, {3, "three"}};
for (auto it = m.begin(); it != m.end(); ++it) {
if (it->second == "two") {
m.erase(it); // Iterator invalidated, but loop continues!
break; // Must break immediately
}
}
}
โ CORRECT:
void good_vector_operations() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// Method 1: erase-remove idiom
vec.erase(std::remove_if(vec.begin(), vec.end(),
[](int n) { return n % 2 == 0; }), vec.end());
// Method 2: iterate backwards
for (auto it = vec.rbegin(); it != vec.rend(); ++it) {
if (*it % 2 == 0) {
vec.erase(std::next(it).base());
}
}
// Method 3: use erase return value
for (auto it = vec.begin(); it != vec.end();) {
if (*it % 2 == 0) {
it = vec.erase(it); // erase returns next valid iterator
} else {
++it;
}
}
}
void good_map_operations() {
std::map<int, std::string> m = {{1, "one"}, {2, "two"}, {3, "three"}};
// Method 1: find and erase
auto it = m.find(2);
if (it != m.end()) {
m.erase(it);
}
// Method 2: use erase return value (C++11+)
for (auto it = m.begin(); it != m.end();) {
if (it->second == "two") {
it = m.erase(it); // Returns next valid iterator
} else {
++it;
}
}
// Method 3: collect keys to erase (safest)
std::vector<int> keys_to_erase;
for (const auto& pair : m) {
if (pair.second == "two") {
keys_to_erase.push_back(pair.first);
}
}
for (int key : keys_to_erase) {
m.erase(key);
}
}
10. Incorrect Container Choice
โ WRONG:
void bad_container_usage() {
// Using vector for frequent insertions/deletions in middle
std::vector<int> vec;
for (int i = 0; i < 10000; ++i) {
vec.insert(vec.begin(), i); // O(n) operation each time!
}
// Using map when unordered_map would be better
std::map<std::string, int> word_count; // O(log n) operations
// for simple key-value lookups without ordering requirement
// Using list for random access
std::list<int> list = {1, 2, 3, 4, 5};
auto it = list.begin();
std::advance(it, 100); // O(n) operation!
}
โ CORRECT:
void good_container_usage() {
// Use deque for frequent front insertions
std::deque<int> deq;
for (int i = 0; i < 10000; ++i) {
deq.push_front(i); // O(1) operation
}
// Use unordered_map for simple key-value lookups
std::unordered_map<std::string, int> word_count; // O(1) average operations
// Use vector for random access
std::vector<int> vec = {1, 2, 3, 4, 5};
int value = vec[100]; // O(1) operation
// Use appropriate container for use case:
// - vector: random access, cache-friendly
// - deque: efficient front/back operations
// - list: frequent middle insertions/deletions
// - set/map: ordered data with fast search
// - unordered_set/map: fastest search, no ordering
}
๐ง Exception Safety Issues
11. Exception Safety Violations
โ WRONG:
class BadClass {
private:
int* ptr1;
int* ptr2;
public:
void unsafe_operation() {
delete ptr1;
ptr1 = new int(10);
delete ptr2;
ptr2 = new int(20); // If this throws, ptr1 is valid but ptr2 is invalid!
// Object is in inconsistent state
}
void another_unsafe_operation() {
int* temp = new int(42); // If this throws, object unchanged (good)
delete ptr1; // If destructor throws, temp leaks!
ptr1 = temp;
}
};
โ CORRECT:
class GoodClass {
private:
std::unique_ptr<int> ptr1;
std::unique_ptr<int> ptr2;
public:
void safe_operation() {
// Create new objects first
auto new_ptr1 = std::make_unique<int>(10);
auto new_ptr2 = std::make_unique<int>(20);
// Only assign if both succeed (strong exception safety)
ptr1 = std::move(new_ptr1);
ptr2 = std::move(new_ptr2);
}
void copy_and_swap_idiom(const GoodClass& other) {
// Create copy
GoodClass temp(other); // If this throws, *this unchanged
// Non-throwing swap
std::swap(ptr1, temp.ptr1);
std::swap(ptr2, temp.ptr2);
// temp destroys old data
}
};
12. Resource Leaks in Constructors
โ WRONG:
class BadResource {
private:
FILE* file1;
FILE* file2;
int* buffer;
public:
BadResource(const char* filename1, const char* filename2) {
file1 = fopen(filename1, "r");
if (!file1) throw std::runtime_error("Cannot open file1");
file2 = fopen(filename2, "r");
if (!file2) throw std::runtime_error("Cannot open file2"); // file1 leaks!
buffer = new int[1000];
// If this throws, both files leak!
}
~BadResource() {
fclose(file1);
fclose(file2);
delete[] buffer;
}
};
โ CORRECT:
// RAII wrapper for FILE*
class FileWrapper {
private:
FILE* file;
public:
explicit FileWrapper(const char* filename, const char* mode)
: file(fopen(filename, mode)) {
if (!file) {
throw std::runtime_error("Cannot open file");
}
}
~FileWrapper() {
if (file) fclose(file);
}
FILE* get() const { return file; }
// Non-copyable, movable
FileWrapper(const FileWrapper&) = delete;
FileWrapper& operator=(const FileWrapper&) = delete;
FileWrapper(FileWrapper&& other) noexcept : file(other.file) {
other.file = nullptr;
}
FileWrapper& operator=(FileWrapper&& other) noexcept {
if (this != &other) {
if (file) fclose(file);
file = other.file;
other.file = nullptr;
}
return *this;
}
};
class GoodResource {
private:
FileWrapper file1;
FileWrapper file2;
std::unique_ptr<int[]> buffer;
public:
GoodResource(const char* filename1, const char* filename2)
: file1(filename1, "r") // If this throws, nothing to clean up
, file2(filename2, "r") // If this throws, file1 auto-cleaned
, buffer(std::make_unique<int[]>(1000)) // If this throws, both files auto-cleaned
{
// Constructor body - all resources already acquired safely
}
// Destructor automatically generated - all RAII
};
๐ฏ Modern C++ (C++11+) Pitfalls
13. Move Semantics Misuse
โ WRONG:
class BadMoveExample {
private:
std::vector<int> data;
std::string name;
public:
// Wrong: should be noexcept
BadMoveExample(BadMoveExample&& other) {
data = std::move(other.data);
name = std::move(other.name);
// If this throws, other is in moved-from state but constructor failed
}
// Wrong: should be noexcept
BadMoveExample& operator=(BadMoveExample&& other) {
data = std::move(other.data);
name = std::move(other.name);
return *this;
}
void bad_usage() {
std::vector<int> vec = {1, 2, 3};
data = std::move(vec);
// Bug: using vec after move
std::cout << vec.size() << std::endl; // Undefined behavior!
}
};
โ CORRECT:
class GoodMoveExample {
private:
std::vector<int> data;
std::string name;
public:
// Correct: noexcept move constructor
GoodMoveExample(GoodMoveExample&& other) noexcept
: data(std::move(other.data))
, name(std::move(other.name)) {
// Member initializer list is exception-safe
}
// Correct: noexcept move assignment
GoodMoveExample& operator=(GoodMoveExample&& other) noexcept {
if (this != &other) {
data = std::move(other.data);
name = std::move(other.name);
}
return *this;
}
void good_usage() {
std::vector<int> vec = {1, 2, 3};
data = std::move(vec);
// Don't use vec after move, or check if it's in valid state
vec.clear(); // Reset to known state if needed
}
};
14. Auto and Type Deduction Issues
โ WRONG:
void auto_pitfalls() {
// Unexpected types
auto x = 1; // int, not long
auto y = 1.0f; // float, not double
std::vector<bool> vec = {true, false, true};
auto element = vec[0]; // Not bool! It's std::vector<bool>::reference
bool flag = element; // This can cause issues
// Dangling references
auto& ref = std::string("temporary"); // Dangling reference!
// Copy when you wanted reference
std::vector<std::string> strings = {"hello", "world"};
for (auto item : strings) { // Copies each string!
item += " modified"; // Modifies copy, not original
}
}
โ CORRECT:
void auto_best_practices() {
// Be explicit when type matters
auto x = 1L; // long
auto y = 1.0; // double
std::vector<bool> vec = {true, false, true};
bool element = vec[0]; // Explicit conversion
// Use auto&& for forwarding references
auto&& ref = std::string("temporary"); // OK, extends lifetime
// Use references in range-based for loops
std::vector<std::string> strings = {"hello", "world"};
for (auto& item : strings) { // Reference, no copy
item += " modified"; // Modifies original
}
for (const auto& item : strings) { // Const reference for read-only
std::cout << item << std::endl;
}
}
๐งฎ Numeric & Performance Issues
15. Integer Overflow and Undefined Behavior
โ WRONG:
void numeric_problems() {
// Signed integer overflow (undefined behavior)
int max_int = std::numeric_limits<int>::max();
int overflow = max_int + 1; // Undefined behavior!
// Array bounds issues
std::vector<int> vec(10);
int index = -1;
vec[index] = 42; // Undefined behavior!
// Uninitialized variables
int uninit;
int result = uninit * 2; // Undefined behavior!
// Null pointer dereference
int* ptr = nullptr;
*ptr = 42; // Undefined behavior!
}
โ CORRECT:
#include <limits>
#include <stdexcept>
void safe_numeric_operations() {
// Check for overflow
int max_int = std::numeric_limits<int>::max();
if (max_int == std::numeric_limits<int>::max()) {
throw std::overflow_error("Would overflow");
}
// Use checked arithmetic or larger types
long long safe_result = static_cast<long long>(max_int) + 1;
// Bounds checking
std::vector<int> vec(10);
int index = -1;
if (index >= 0 && index < static_cast<int>(vec.size())) {
vec[index] = 42;
} else {
throw std::out_of_range("Index out of bounds");
}
// Initialize variables
int value = 0; // Always initialize
int result = value * 2;
// Check pointers before use
int* ptr = get_pointer();
if (ptr != nullptr) {
*ptr = 42;
}
}
16. Performance Anti-patterns
โ WRONG:
void performance_problems() {
// Unnecessary copies
std::vector<std::string> get_strings();
std::vector<std::string> strings = get_strings(); // Copy
for (std::string s : strings) { // Copy each string
std::cout << s << std::endl;
}
// Inefficient string concatenation
std::string result;
for (int i = 0; i < 1000; ++i) {
result += std::to_string(i) + " "; // Multiple allocations
}
// Wrong container choice
std::vector<int> vec;
for (int i = 0; i < 1000; ++i) {
vec.insert(vec.begin(), i); // O(n) each time!
}
}
โ CORRECT:
void performance_optimized() {
// Move semantics
std::vector<std::string> get_strings();
auto strings = get_strings(); // Move if possible
for (const auto& s : strings) { // No copies
std::cout << s << std::endl;
}
// Efficient string building
std::ostringstream oss;
for (int i = 0; i < 1000; ++i) {
oss << i << " ";
}
std::string result = oss.str();
// Reserve capacity when known
std::vector<int> vec;
vec.reserve(1000); // Avoid multiple reallocations
for (int i = 0; i < 1000; ++i) {
vec.push_back(i);
}
// Use appropriate container
std::deque<int> deq; // Better for front insertions
for (int i = 0; i < 1000; ++i) {
deq.push_front(i); // O(1) each time
}
}
๐ก๏ธ Best Practices Summary
Modern C++ Guidelines
```cpp // 1. Prefer smart pointers over raw pointers std::unique_ptr ptr = std::make_unique(42); std::shared_ptr shared = std::make_shared(42);
// 2. Use RAII for all resources class FileResource { std::unique_ptr file; public: FileResource(const char* name) : file(fopen(name, "r"), &fclose) {} };
// 3
Subscribe to my newsletter
Read articles from Anni Huang directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Anni Huang
Anni Huang
I am Anni HUANG, a software engineer with 3 years of experience in IDE development and Chatbot.