C++ Copy & Move Constructors: Rule of Five, RAII, and noexcept

C++ Copy & Move Constructors: Rule of Five, RAII, and noexcept

이 글의 핵심

Practical guide to copy and move special members: RAII types, noexcept moves for containers, shallow-copy bugs, and Rule of Zero with smart pointers.

Rule of Five

Types that manage resources should define the five special members (constructor, destructor, copy ctor, copy assign, move ctor, move assign—often counted as five “operations” plus destructor).

class Resource {
private:
    int* data;
    size_t size;
    
public:
    Resource(size_t s) : size(s), data(new int[s]) {}
    
    ~Resource() {
        delete[] data;
    }
    
    Resource(const Resource& other) 
        : size(other.size), data(new int[other.size]) {
        copy(other.data, other.data + size, data);
    }
    
    Resource& operator=(const Resource& other) {
        if (this != &other) {
            delete[] data;
            size = other.size;
            data = new int[size];
            copy(other.data, other.data + size, data);
        }
        return *this;
    }
    
    Resource(Resource&& other) noexcept
        : size(other.size), data(other.data) {
        other.data = nullptr;
        other.size = 0;
    }
    
    Resource& operator=(Resource&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            other.data = nullptr;
            other.size = 0;
        }
        return *this;
    }
};

Copy constructor

class String {
private:
    char* data;
    size_t length;
    
public:
    String(const char* str) {
        length = strlen(str);
        data = new char[length + 1];
        strcpy(data, str);
    }
    
    String(const String& other) {
        length = other.length;
        data = new char[length + 1];
        strcpy(data, other.data);
        cout << "copy ctor" << endl;
    }
    
    ~String() {
        delete[] data;
    }
};

int main() {
    String s1("Hello");
    String s2 = s1;  // copy ctor
    String s3(s1);   // copy ctor
}

Move constructor

class String {
private:
    char* data;
    size_t length;
    
public:
    String(const char* str) {
        length = strlen(str);
        data = new char[length + 1];
        strcpy(data, str);
    }
    
    String(String&& other) noexcept {
        data = other.data;
        length = other.length;
        
        other.data = nullptr;
        other.length = 0;
        
        cout << "move ctor" << endl;
    }
    
    ~String() {
        delete[] data;
    }
};

int main() {
    String s1("Hello");
    String s2 = move(s1);  // move ctor
}

Practical examples

Example 1: Dynamic array

template<typename T>
class DynamicArray {
private:
    T* data;
    size_t size;
    size_t capacity;
    
public:
    DynamicArray(size_t cap = 10) 
        : size(0), capacity(cap), data(new T[cap]) {}
    
    ~DynamicArray() {
        delete[] data;
    }
    
    DynamicArray(const DynamicArray& other)
        : size(other.size), capacity(other.capacity), 
          data(new T[other.capacity]) {
        copy(other.data, other.data + size, data);
    }
    
    DynamicArray& operator=(const DynamicArray& other) {
        if (this != &other) {
            delete[] data;
            size = other.size;
            capacity = other.capacity;
            data = new T[capacity];
            copy(other.data, other.data + size, data);
        }
        return *this;
    }
    
    DynamicArray(DynamicArray&& other) noexcept
        : size(other.size), capacity(other.capacity), data(other.data) {
        other.data = nullptr;
        other.size = 0;
        other.capacity = 0;
    }
    
    DynamicArray& operator=(DynamicArray&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            capacity = other.capacity;
            other.data = nullptr;
            other.size = 0;
            other.capacity = 0;
        }
        return *this;
    }
    
    void push_back(const T& value) {
        if (size == capacity) {
            resize();
        }
        data[size++] = value;
    }
    
    T& operator {
        return data[index];
    }
    
    size_t getSize() const {
        return size;
    }
    
private:
    void resize() {
        capacity *= 2;
        T* newData = new T[capacity];
        copy(data, data + size, newData);
        delete[] data;
        data = newData;
    }
};

int main() {
    DynamicArray<int> arr1;
    arr1.push_back(1);
    arr1.push_back(2);
    
    DynamicArray<int> arr2 = arr1;      // copy
    DynamicArray<int> arr3 = move(arr1); // move
}

Example 2: File handle

class FileHandle {
private:
    FILE* file;
    string filename;
    
public:
    FileHandle(const string& name) : filename(name) {
        file = fopen(name.c_str(), "r");
        if (!file) {
            throw runtime_error("failed to open file");
        }
    }
    
    ~FileHandle() {
        if (file) {
            fclose(file);
        }
    }
    
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    
    FileHandle(FileHandle&& other) noexcept
        : file(other.file), filename(move(other.filename)) {
        other.file = nullptr;
    }
    
    FileHandle& operator=(FileHandle&& other) noexcept {
        if (this != &other) {
            if (file) {
                fclose(file);
            }
            file = other.file;
            filename = move(other.filename);
            other.file = nullptr;
        }
        return *this;
    }
    
    string read() {
        if (!file) return "";
        
        fseek(file, 0, SEEK_END);
        long size = ftell(file);
        fseek(file, 0, SEEK_SET);
        
        string content(size, '\0');
        fread(&content[0], 1, size, file);
        
        return content;
    }
};

int main() {
    FileHandle fh1("test.txt");
    // FileHandle fh2 = fh1;  // error: copy deleted
    FileHandle fh2 = move(fh1);  // OK
}

Example 3: Tiny unique_ptr-like type

template<typename T>
class UniquePtr {
private:
    T* ptr;
    
public:
    explicit UniquePtr(T* p = nullptr) : ptr(p) {}
    
    ~UniquePtr() {
        delete ptr;
    }
    
    UniquePtr(const UniquePtr&) = delete;
    UniquePtr& operator=(const UniquePtr&) = delete;
    
    UniquePtr(UniquePtr&& other) noexcept : ptr(other.ptr) {
        other.ptr = nullptr;
    }
    
    UniquePtr& operator=(UniquePtr&& other) noexcept {
        if (this != &other) {
            delete ptr;
            ptr = other.ptr;
            other.ptr = nullptr;
        }
        return *this;
    }
    
    T& operator*() const {
        return *ptr;
    }
    
    T* operator->() const {
        return ptr;
    }
    
    T* get() const {
        return ptr;
    }
    
    T* release() {
        T* temp = ptr;
        ptr = nullptr;
        return temp;
    }
};

int main() {
    UniquePtr<int> p1(new int(42));
    // UniquePtr<int> p2 = p1;  // error
    UniquePtr<int> p2 = move(p1);  // OK
    
    cout << *p2 << endl;  // 42
}

Copy elision

String createString() {
    return String("Hello");  // may elide copy/move
}

int main() {
    String s = createString();  // often no copy/move (C++17 guaranteed in many cases)
}

Common pitfalls

Pitfall 1: Shallow copy

// Bad: shallow copy (default copy ctor)
class BadString {
private:
    char* data;
    
public:
    BadString(const char* str) {
        data = new char[strlen(str) + 1];
        strcpy(data, str);
    }
    
    ~BadString() {
        delete[] data;
    }
};

BadString s1("Hello");
BadString s2 = s1;  // same pointer
// double free on destruction

// Good: deep copy
class GoodString {
private:
    char* data;
    
public:
    GoodString(const char* str) {
        data = new char[strlen(str) + 1];
        strcpy(data, str);
    }
    
    GoodString(const GoodString& other) {
        data = new char[strlen(other.data) + 1];
        strcpy(data, other.data);
    }
    
    ~GoodString() {
        delete[] data;
    }
};

Pitfall 2: Self-assignment

// Bad: no self-check
Resource& operator=(const Resource& other) {
    delete[] data;  // breaks on self-assign
    size = other.size;
    data = new int[size];
    copy(other.data, other.data + size, data);
    return *this;
}

// Good
Resource& operator=(const Resource& other) {
    if (this != &other) {
        delete[] data;
        size = other.size;
        data = new int[size];
        copy(other.data, other.data + size, data);
    }
    return *this;
}

Pitfall 3: Missing noexcept

// Without noexcept
Resource(Resource&& other) {
    // ...
}

// With noexcept
Resource(Resource&& other) noexcept {
    // ...
}

// Reason: std::vector prefers noexcept moves when reallocating

FAQ

Q1: When is Rule of Five needed?

A: When you directly manage resources (memory, handles, etc.).

Q2: Copy vs move?

A:

  • Copy: independent duplicate
  • Move: transfer ownership—usually faster

Q3: = delete?

A: To forbid copy/move—like unique_ptr.

Q4: Why noexcept?

A: Exception safety and container optimizations.

Q5: Rule of Zero?

A: Use smart pointers and RAII wrappers—often no manual special members.

Q6: Learning resources?

A:

  • Effective Modern C++
  • cppreference.com
  • C++ Primer

  • Algorithm copy
  • default and delete
  • Custom deleters

Practical tips

Debugging

  • Warnings first

Performance

  • Profile

Code review

  • Conventions

Practical checklist

Before coding

  • Right approach?
  • Maintainable?
  • Performance?

While coding

  • Warnings?
  • Edge cases?
  • Errors?

At review

  • Intent?
  • Tests?
  • Docs?

Keywords

C++, copy constructor, move constructor, RAII, Rule of Five


  • Class and objects
  • new vs malloc
  • shared_ptr vs unique_ptr
  • malloc vs new vs make_unique
  • Custom deleters