C++ programming: Common Mistakes

Anni HuangAnni Huang
14 min read

๐Ÿ’พ 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

0
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.