C++ Copy & Move Constructors: Rule of Five and RAII
Key takeaways
Rule of Five in C++: copy/move constructors and assignment, deep copy vs shallow, self-assignment, noexcept moves, copy elision, and FileHandle patterns.
Every C++ programmer who manages a raw resource eventually hits the same bug: a program that runs fine, then crashes with a double-free or a heap corruption message the moment a vector resizes or a function returns an object by value. The root cause is almost always the same — the type’s copy or move behavior was left to the compiler’s defaults when it shouldn’t have been, or a hand-written special member function has a subtle logic gap. This guide works through the Rule of Five end to end: why it exists, exactly when the compiler writes these functions for you (and when it silently refuses to), what a move constructor actually does at the machine level, why noexcept is not just documentation but a behavioral switch that container code reads at compile time, and the self-assignment/self-move bugs that slip past code review because they only trigger in rare call patterns. The code blocks omit #include lines and std:: qualification (copy, cout, move) to stay short, so add using namespace std; or the qualifiers when you try them, and read the prose around each one before copying it into a project.
From the Rule of Three to the Rule of Five
Before C++11, C++ had the Rule of Three: if a class needs a custom destructor, it almost certainly also needs a custom copy constructor and copy assignment operator, because owning a resource (heap memory, a file handle, a socket) means the compiler-generated member-wise copy is wrong. The compiler’s default copy constructor just copies each data member verbatim — for a raw pointer, that copies the address, not the data it points to. Two objects then share one buffer, and when both destructors run, the second delete frees memory that is already gone.
C++11 added rvalue references and two more special members — the move constructor and move assignment operator — turning the Rule of Three into the Rule of Five. The motivation was performance, not correctness: before move semantics, returning a std::vector or std::string by value, or growing a container, meant a full deep copy even when the source object was about to be destroyed anyway (a temporary, or a local variable being returned). A move constructor lets the compiler recognize “this source object is disposable” and simply steal its internals instead of duplicating them.
Types that manage resources should define the five special members
The five special members are the destructor, copy constructor, copy assignment operator, move constructor and move assignment operator. A type that manages a resource directly usually needs to define all five (ordinary constructors are not part of the count).
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;
}
};
Resource is the canonical teaching example because it owns exactly one thing — a heap array — and every one of the five members exists to answer the same question in a different context: “what does it mean to duplicate or transfer ownership of data?” The destructor frees it once. The copy constructor and copy assignment allocate a brand-new buffer and duplicate every element, so the two objects never alias the same memory — that is the deep copy that the compiler-generated default would not give you. The move constructor and move assignment do the opposite of copying on purpose: they take the pointer value itself (data(other.data)), then immediately null out other.data. After a move, other is left owning nothing, so its destructor’s delete[] data is a safe no-op (delete on nullptr is defined to do nothing). That null-out step is not optional cleanup — without it, both objects would hold the same pointer, and you are back to the double-free bug the Rule of Three was invented to prevent.
The implicit generation and deletion rules
The trap most developers fall into is assuming that declaring one special member is harmless and the compiler will fill in sensible defaults for the rest. It will not, and the exact rules matter:
- If you declare none of the five (and no destructor), the compiler generates all five as member-wise operations. This is the Rule of Zero — the ideal state, usually achieved by only holding members that already manage themselves (
std::vector,std::string,std::unique_ptr). - If you declare a destructor (even an empty one, even just to log something), the compiler still generates the copy constructor and copy assignment operator for backward compatibility with pre-C++11 code — but it does not generate the move constructor or move assignment operator. This is an easy way to lose performance by accident: you write
~Widget() { log("destroyed"); }for debugging, and everystd::move(widget)silently falls back to a full copy because there is no move constructor to call. The code still compiles and still runs correctly, which is exactly why nobody notices until a profiler shows the copies. - If you declare any copy member (copy constructor or copy assignment), the compiler again suppresses the implicit move members, for the same reason — a hand-written copy operation strongly signals the defaults are unsafe, so the compiler refuses to guess at a move implementation.
- If you declare any move member (move constructor or move assignment), the compiler goes further and marks the copy constructor and copy assignment as
= deleted, not just “not generated.” A type with a hand-rolled move constructor and no copy constructor of its own becomes move-only by default — attemptingWidget b = a;fails to compile with “use of deleted function,” which is precisely howstd::unique_ptrenforces single ownership.
The practical takeaway: once you write any of the five by hand, decide explicitly what you want for all five, and spell it out with = default or = delete rather than relying on which implicit rule happens to apply. Resource above declares all five explicitly for exactly this reason — nothing is left to the reader to infer from the suppression rules.
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
}
A copy constructor’s job is to produce a fully independent duplicate — after String s2 = s1; completes, mutating s1 must never be observable through s2, and destroying either one must not affect the other. String’s copy constructor does this the only correct way for an owned char*: it allocates a new buffer sized for other.length + 1 and calls strcpy to duplicate the actual bytes, rather than copying the pointer. Both String s2 = s1; (copy-initialization) and String s3(s1); (direct-initialization) invoke the same copy constructor — they are two spellings of the same operation, and the cout line makes that call visible when you run the example.
This is also where the when to use it question really has two answers. If a class’s members are all value types or already resource-managing types (std::string, std::vector<T>, std::unique_ptr<T>), you almost never write a copy constructor by hand — the compiler-generated member-wise copy already calls each member’s own copy constructor correctly, which is Rule-of-Zero territory. You write one by hand only when a member is a raw resource handle (a pointer from new[], a FILE*, a socket descriptor, a mutex) that has no idea how to duplicate itself, which is exactly String’s situation with char* data.
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
}
It helps to think about what actually happens in memory when this move constructor runs, as opposed to the copy constructor above. The copy constructor calls new char[length + 1] (a heap allocation) followed by strcpy (an O(n) byte-by-byte copy of the string’s contents). The move constructor does neither: data = other.data; is a single pointer assignment — copying eight bytes on a 64-bit platform, regardless of whether the string holds 5 characters or 5 gigabytes. There is no new allocation and no byte copying of the payload at all. This is the entire performance argument for move semantics in one line: a copy is O(n) in the size of the resource, a move is O(1). The trade-off is that the source object, other, must be left in a state where its destructor won’t double-free the buffer that s2 now owns — hence other.data = nullptr; immediately after the “steal.” The standard library guarantees that its own moved-from objects are in a valid but unspecified state, and the same contract is the sensible default for your types: a moved-from object must at least be safe to destroy and to assign to, but callers should not rely on its contents.
std::move(s1) itself does no moving — it is purely a cast. std::move converts s1 (an lvalue, since it has a name) into an rvalue reference, which is what makes the compiler’s overload resolution pick String(String&&) instead of String(const String&). The actual “stealing” happens entirely inside the move constructor’s body, which is why the constructor exists at all — std::move just grants permission to cannibalize the argument.
Why noexcept is not optional documentation
The noexcept on both Resource’s and String’s move constructors above is doing real work, not just advertising intent. std::vector gives a strong exception guarantee during reallocation: if growing the vector throws partway through, the vector must be left exactly as it was before the call, with no partially-moved elements. To honor that guarantee, std::vector checks — at compile time, via std::is_nothrow_move_constructible — whether your type’s move constructor is marked noexcept. If it is, the vector moves each element into the new, larger buffer, which is fast and cannot break the guarantee (a noexcept function that throws anyway calls std::terminate, so the compiler can treat it as non-throwing). If it is not marked noexcept, and a copy constructor exists, std::vector falls back to copying every element instead — silently. Your code still compiles, still runs, and produces the same observable results; it just does an O(n) deep copy per element on every reallocation instead of an O(1) pointer swap, with no warning printed anywhere. That makes it an easy regression to introduce: removing noexcept from a move constructor “just to add a log statement that might throw” makes every std::vector<T> of that type quietly slower to grow. Note that a move constructor declared = default (or implicitly generated) is automatically noexcept when all members’ move constructors are, so the Rule-of-Zero types get this right for free.
flowchart TD
A["std::vector needs to grow<br/>(push_back exceeds current capacity)"] --> B{"Is T's move constructor<br/>declared noexcept?"}
B -->|"Yes"| C["Move each element into the<br/>new buffer — O(1) per element"]
B -->|"No, but a copy constructor exists"| D["Copy each element into the<br/>new buffer — O(n) per element, silently"]
B -->|"No, and copy is deleted/unavailable"| E["Move anyway — no safe fallback exists,<br/>so the strong exception guarantee is dropped"]
The one exception to the “falls back to copy” rule is a move-only type like std::unique_ptr or FileHandle below, whose copy constructor is = deleted: std::vector has no copy to fall back to, so it moves the elements regardless of noexcept, accepting that the strong exception guarantee cannot be upheld in that case. The practical rule is simple — always mark move constructors and move assignment operators noexcept unless something inside them can genuinely throw (which should be rare, since a move constructor typically only reassigns pointers and primitives).
Copy vs move: which constructor actually gets called
flowchart TD
A["T obj2 = obj1;"] --> B{"Is the right-hand<br/>expression an lvalue or rvalue?"}
B -->|"lvalue — a named variable"| C["Copy constructor<br/>T(const T&) is selected"]
B -->|"rvalue — a temporary, or<br/>the result of std::move(obj1)"| D{"Is a move constructor<br/>declared and accessible?"}
D -->|"Yes"| E["Move constructor T(T&&) —<br/>pointer/handle is stolen,<br/>source is nulled out"]
D -->|"No — only copy exists"| C
C --> F["Deep copy: new allocation,<br/>every byte duplicated — O(n)"]
E --> G["Shallow transfer: pointer<br/>value copied, source zeroed — O(1)"]
Overload resolution, not the programmer, decides which constructor runs. String s2 = s1; binds to the copy constructor because s1 is a named variable (an lvalue). String s2 = move(s1); binds to the move constructor because std::move produces an rvalue reference — even though s1 is still the exact same object with the exact same name, casting it to an rvalue tells the compiler “treat this as disposable.” A function returning String createString() by value also produces an rvalue at the call site, which is why String s = createString(); prefers the move constructor over the copy constructor when no further optimization applies (see copy elision below, which can skip both).
Dynamic Array, File Handle, and a Tiny unique_ptr
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[](size_t index) {
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
}
DynamicArray is a miniature, teaching-sized version of what std::vector does internally, and it is worth studying precisely because it exposes the mechanics std::vector hides. Notice that resize() — called internally whenever push_back outgrows capacity — allocates a new buffer and copies every existing element into it, then frees the old buffer. This is the exact same “grow and migrate” operation std::vector performs, and it is exactly why the noexcept discussion above matters for a type like this: if T’s move constructor were not noexcept, a DynamicArray<T> growing internally (or a std::vector<DynamicArray<T>> growing around it) would silently prefer copying T over moving it. DynamicArray<int> arr2 = arr1; triggers the copy constructor, allocating an entirely separate buffer sized to other.capacity — arr1 and arr2 share no memory afterward. DynamicArray<int> arr3 = move(arr1); triggers the move constructor instead, which just takes arr1’s three fields (data, size, capacity) and zeroes arr1 out; no allocation, no element-by-element copy loop, regardless of how many elements arr1 held. One pitfall worth flagging separately from the Rule of Five itself: the T& operator[](size_t index) overload above does no bounds checking, mirroring std::vector::operator[] (as opposed to the bounds-checked .at()) — passing an out-of-range index is undefined behavior, not a caught exception.
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
}
FileHandle is a textbook RAII wrapper: the constructor acquires the resource (fopen), and the destructor releases it (fclose) unconditionally, so a FileHandle can never leak an open file descriptor regardless of how the enclosing scope exits — normal return, early return, or an exception unwinding the stack. This is the direct connection between RAII and the Rule of Five: RAII promises “the resource’s lifetime is tied to the object’s lifetime,” and the copy/move members are what keep that promise intact when the object itself gets duplicated or relocated.
Here, copying is deliberately forbidden with FileHandle(const FileHandle&) = delete;. This is the right design choice for a FILE*: there is no meaningful way to “duplicate” an open file handle (you’d need dup()-style OS support, and even then two FileHandle objects would independently call fclose on the same underlying descriptor, which is a double-close bug analogous to double-free). Deleting the copy operations turns that logic error into a compile-time error — the commented-out line // FileHandle fh2 = fh1; simply does not compile. Moving is still allowed and is cheap: the move constructor takes the FILE* pointer and hands filename off via its own move (since std::string is itself move-enabled), then nulls other.file so the moved-from FileHandle’s destructor sees file == nullptr and skips fclose — the same “null out the source” pattern as Resource and String, applied to an OS handle instead of a heap pointer.
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
}
This minimal reimplementation makes the point that std::unique_ptr is not a compiler intrinsic — it is an ordinary class built entirely out of the same Rule-of-Five vocabulary used throughout this article, plus operator overloads (*, ->) that make it behave syntactically like a raw pointer. Single ownership is enforced exactly the way FileHandle enforces it: = delete on both copy operations, so UniquePtr<int> p2 = p1; fails to compile rather than producing two owners of the same new int(42). The release() member is worth calling out because it is a different operation from moving: release() hands back the raw pointer and sets the internal ptr to nullptr without transferring ownership to another UniquePtr — the caller becomes responsible for the memory (this mirrors std::unique_ptr::release(), typically used when handing a pointer to a C API that wants raw ownership). Confusing release() with get() is a real-world leak/double-free source: get() only observes the pointer and leaves this object still owning it, so calling delete ptr.get() yourself while the UniquePtr is still alive causes the same double-free the class exists to prevent.
Copy elision
Even a well-written, noexcept move constructor is not the fastest possible outcome — the fastest outcome is calling no constructor at all for the temporary, which is what copy elision achieves. When a function constructs its return value directly from a temporary expression, as createString() does below, the compiler is permitted (and since C++17, in this specific case, required) to construct the object directly in the caller’s storage, skipping both the copy and the move entirely.
String createString() {
return String("Hello"); // may elide copy/move
}
int main() {
String s = createString(); // often no copy/move (C++17 guaranteed in many cases)
}
Two related but distinct optimizations are at play here. Guaranteed copy elision (C++17) applies to a prvalue like String("Hello") being returned directly and then used to initialize s — the standard now specifies that no temporary materializes at all, so this case requires no accessible copy or move constructor whatsoever (even a type with both = deleted would compile here). NRVO (Named Return Value Optimization) is the older, still-optional case where a named local variable is returned (String tmp(...); return tmp;) — compilers generally perform it, but the standard does not mandate it, so the type must still have an accessible copy or move constructor as a fallback. The practical implication for the Rule of Five: don’t assume that seeing “no copy ctor / move ctor” printed in a test program means your constructors are unreachable dead code — elision is an optimization the compiler applies opportunistically, and the exact same code path can fall back to an actual copy or move under a debug build, a different compiler, or a slightly different return expression.
Shallow Copies, Self-Assignment, and Missing noexcept
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;
}
};
BadString declares a destructor but no copy constructor, so — per the implicit-generation rules covered earlier — the compiler still generates a copy constructor for it, and that generated copy constructor does a member-wise copy of char* data. A member-wise copy of a pointer copies the address, not the bytes it points to, so s1 and s2 end up pointing at the same heap allocation. Nothing crashes at the BadString s2 = s1; line itself; the bug only surfaces later, when both objects go out of scope: the first destructor’s delete[] data frees the buffer correctly, and the second destructor’s delete[] data then frees already-freed memory — a double free, which is undefined behavior that might crash immediately, corrupt unrelated heap state, or (worst case) run silently for months until a build/allocator change makes it visible. GoodString fixes this the same way String and Resource do above: the copy constructor allocates its own buffer and duplicates the contents, so no two GoodString objects can ever share ownership of one buffer.
Pitfall 2: Self-assignment (and self-move)
// 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;
}
r = r; looks pointless, but code that never writes it literally can still trigger it indirectly — through a reference or pointer alias (someFunc(Resource& a, Resource& b) { a = b; } called as someFunc(r, r)), through container algorithms that reassign elements in place, or through generic template code that doesn’t know at compile time whether its two arguments happen to be the same object. Without the this != &other guard, the “bad” version deletes data first — but other is this, so other.data was just freed too. The subsequent copy(other.data, ...) reads from memory that was already released, and the object is left in a broken state. The guard is cheap (one pointer comparison) and makes the operation safe regardless of aliasing. The “good” version still has a weaker flaw: it deletes the old buffer before allocating the new one, so if new int[size] throws std::bad_alloc, the object is left holding a dangling data pointer. Allocating and copying into a temporary first and deleting the old buffer only afterward (or using the copy-and-swap idiom) gives the strong exception guarantee and handles self-assignment without a special check.
The same hazard exists for move assignment, and it is easier to trigger by accident because std::move on the same variable is easy to write without noticing: obj = std::move(obj); (or, less obviously, container[i] = std::move(container[j]) where i == j at runtime). Resource::operator=(Resource&&), DynamicArray’s and FileHandle’s move assignments above all guard with if (this != &other) — walk through what happens without that guard: delete[] data; frees the buffer, then data = other.data; reads other.data, which is this->data, a pointer that was just freed and is now dangling. The object ends up “owning” a pointer to memory it no longer controls. Self-move is rarer in practice than self-copy, but std::swap-based algorithms and generic code that moves ranges around (including some hand-rolled sort or dedup routines) can produce it, so treating the guard as mandatory for move assignment — not just copy assignment — is the safer default.
Pitfall 3: Missing noexcept
// Without noexcept
Resource(Resource&& other) {
// ...
}
// With noexcept
Resource(Resource&& other) noexcept {
// ...
}
// Reason: std::vector prefers noexcept moves when reallocating
This is the same behavior described in detail earlier in the move-constructor section, condensed to its minimal reproducible form: the only textual difference between the two declarations is the noexcept keyword, but that keyword is what std::is_nothrow_move_constructible<Resource> inspects, and what std::vector<Resource> consults before deciding whether to move or copy elements during a reallocation. There is no compiler warning for omitting it — the code compiles identically either way, and the only symptom is that a profiler or a benchmark shows more allocations and more copy-constructor calls than the move-heavy code path would suggest. Treat noexcept on move operations as a correctness-adjacent property of the type, not a stylistic flourish.
RAII and the Rule of Five
Stepping back, RAII (Resource Acquisition Is Initialization) and the Rule of Five are two halves of the same idea. RAII says a resource’s lifetime should be bound to an object’s lifetime — acquire in the constructor, release in the destructor, so the resource cannot leak regardless of how control leaves the scope. The Rule of Five is what makes that promise survive contact with copying, assignment, and moving: without correct copy/move semantics, an RAII object can still double-free (if two owners both try to release a shared resource), use a resource after another owner released it, or leak (if an assignment overwrites a handle without releasing the old one). FileHandle and UniquePtr above are both RAII types that solve this by refusing to copy at all and only allowing a single, trackable transfer of ownership via move — which is also exactly how std::unique_ptr, std::lock_guard, and std::fstream behave in the standard library.
In practice, the best way to apply everything above is to need it as rarely as possible. If a class’s members are themselves already RAII types (std::string, std::vector, std::unique_ptr, std::shared_ptr), the Rule of Zero applies: declare no special members at all, and the compiler-generated ones correctly call each member’s own copy/move logic in turn. Hand-writing all five, as Resource, String, DynamicArray, FileHandle, and the toy UniquePtr do in this article, should be reserved for the small number of types at the bottom of an abstraction stack that actually wrap a raw resource — everything built on top of them should be able to stay at Rule of Zero.
For a deeper dive into rvalue references, std::forward, and perfect forwarding beyond what a copy/move constructor needs, see the companion post on C++ move semantics. For the shallow-vs-deep-copy distinction worked through with the copy-and-swap idiom, and an interview-oriented framing of “why do we need move semantics,” see shallow vs. deep copy and move semantics. For RAII patterns beyond copy/move — lock guards, smart pointer ownership models, and scope-based cleanup in general — see the C++ RAII guide. And for the lvalue/rvalue category rules that determine which overload gets picked in the first place, see rvalue vs. lvalue.