🧬Go deeper — read the bookConstructor inheritance, init order & the Rule of 5— runnable code & full walkthrough →⚙️Go deeper — read the bookThe Rule of 0 / 3 / 5— runnable code & full walkthrough →🧰Go deeper — read the bookImplement it yourself: the interview classics— runnable code & full walkthrough →
oopshigh priority

Rule of 5 — Writing a String Class

🔑 Rule of 5: own a raw resource → define dtor + copy ctor + copy assign + move ctor + move assign. Copy = deep (own buffer); move = steal pointer + null source + noexcept; copy-and-swap operator=(by value) is self-assign + exception safe and covers move. Default shallow copy = double free. Prefer Rule of 0: hold std::string and write none.

1 / 6
Rule of 5 — StringWhy Rule of 5 — a class that owns a raw resourcerule of 5 — string — session$ class String { char* data_; size_t size_; }; // Default (shallow) copy is a DISASTER here: String a("hi"); // data_ -> [h i \0] String b = a; // b.data_ = a.data_ (same buffer!) // ~b frees the buffer, then ~a frees it AGAIN -> double free // Rule of 5: own your resource — 5 members work together // ctor (acquire) / dtor (release) / copy x2 / move x2WHAT IT TELLS YOUowns a raw resource
the trigger for Rule of 5 is RAW ownership: new[], fopen, socket(), lock — anything with a paired release
shallow copy = double free
compiler's default copy duplicates the POINTER, not the buffer; two dtors free the same memory
all five or none
if you need one of the five, you need all five; otherwise prefer Rule of 0 (hold a std::string/vector)
!manage a raw resource → define all 5 special members; default copy = shallow = double-freeRule of 0: the BEST class writes none of the five — wrap the resource in std::string / std::vector / unique_ptr and let THEM obey Rule of 5
example.cpp
1class String {
2 char* data_; // owned heap buffer (null-terminated)
3 size_t size_;
4
5public:
6 // 1. Constructor — ACQUIRE the resource
7 String(const char* s = "") : size_(std::strlen(s)) {
8 data_ = new char[size_ + 1];
9 std::memcpy(data_, s, size_ + 1);
10 }
11 // 2. Destructor — RELEASE the resource
12 ~String() {
13 delete[] data_;
14 }
15 // 3. Copy constructor — DEEP copy (own buffer)
16 String(const String& o) : size_(o.size_) {
17 data_ = new char[size_ + 1];
18 std::memcpy(data_, o.data_, size_ + 1);
19 }
20 // 4. Copy assignment — copy-and-swap (self-assign + exception safe)
21 String& operator=(String o) { // by value = the copy
22 swap(*this, o);
23 return *this;
24 }
25 // 5. Move constructor — STEAL the buffer, noexcept
26 String(String&& o) noexcept : data_(o.data_), size_(o.size_) {
27 o.data_ = nullptr;
28 o.size_ = 0; // leave o destructible
29 }
30 // move assignment is covered by operator=(String o) above (by value)
31 friend void swap(String& a, String& b) noexcept {
32 std::swap(a.data_, b.data_);
33 std::swap(a.size_, b.size_);
34 }
35};

Why Rule of 5 — a class that owns a raw resource

String owns a heap buffer (char* data_). The moment a class manages a raw resource (heap memory, file handle, socket, mutex), the compiler-generated special members are WRONG: the default copy does a shallow pointer copy, so two objects free the same buffer → double-free. The Rule of 5 says: if you write any one of {destructor, copy ctor, copy assign, move ctor, move assign}, you almost always need all five. Define them so each object owns exactly one buffer.

Tap ▶ to play · tap the dots or Next → to step

← Previous
C++ Operator Overloading — Member vs Non-member, Spaceship, Conversion Operators
Next →
CRTP: Static vs Dynamic Polymorphism