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
Related posts (internal links)
- 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
Related posts
- Class and objects
- new vs malloc
- shared_ptr vs unique_ptr
- malloc vs new vs make_unique
- Custom deleters