Welcome to our next lesson on building high-performance systems with C++. In our previous session, you successfully implemented a DynamicArray class that manually manages heap memory, giving you direct control over resource allocation and deallocation. However, we ended on a cliffhanger: our class is fundamentally unsafe to copy. Attempting an operation like DynamicArray arr2 = arr1; would lead to disastrous double-free errors.
Today, we will confront this problem head-on by implementing the Rule of Five. You will learn how to define a set of special member functions that give you complete control over your class's copy, move, and destruction behavior. Mastering this is not just about fixing bugs; it's about engineering efficient, predictable, and robust classes, a skill that is absolutely essential for building complex systems like the inference engine you're aiming for. We'll make our DynamicArray not only correct but also highly performant.
1. The Problem: Shallow Copies and the Rule of Three
As you discovered, the DynamicArray class you built in the last lesson has a critical flaw. Let's briefly revisit why. When you write DynamicArray arr2 = arr1;, the compiler, by default, generates a copy constructor for you. This generated function performs a "shallow copy"—it copies the members of the class bit by bit. For primitive types like size_t, this is fine. But for your T* m_Data pointer, it copies the address, not the data it points to.
The result is two DynamicArray objects whose m_Data pointers point to the exact same block of memory on the heap. When the first object goes out of scope, its destructor is called, and it delete[]s the memory. When the second object goes out of scope, its destructor tries to delete[] the same memory again. This is a double-free, which results in undefined behavior and will likely crash your program.
To solve this, we must take control from the compiler and define our own copying logic. This leads us to the classic "Rule of Three" (the precursor to the Rule of Five in older C++). If a class needs a custom implementation for any of the following three, it probably needs all of them:
- Destructor: You've already implemented this to release the memory (
~DynamicArray()). - Copy Constructor: To create a new object as a copy of an existing one (
DynamicArray(const DynamicArray& other)). This is where we'll implement a "deep copy." - Copy Assignment Operator: To assign an existing object's value to another existing object (
operator=(const DynamicArray& other)).
The following CppCon talk segment explains this progression from a simple memory leak to the double-deletion problem.
Back to Basics: The Rule of Five in C++ - Andre Kostur - CppCon 2023
Watch these segments from the talk "Back to Basics: The Rule of Five in C++" by Andre Kostur. He uses a SimpleString class to demonstrate the problems that arise when you don't define custom copy logic.
First, watch how the lack of a destructor leads to a memory leak from this point. Next, see how a shallow copy leads to a double-free error when passing an object by value here. Finally, observe how a similar issue arises with the default copy assignment operator, causing both a memory leak and a double-free here.
To implement these correctly, we'll consult a detailed guide.
Copy/Move Semantics and the Rule of Five | EduC++
This article provides a concise and clear implementation guide for the special member functions.
Read the section The Problem for a recap of the shallow copy issue. Review the Constructor and Destructor sections. This matches what you did in the previous lesson. Focus on the Copy constructor section. Notice how it allocates new memory (new char[other.size_]) and then copies the data over. This is the essence of a deep copy. Finally, study the Copy assignment operator section. It introduces the copy-and-swap idiom, an elegant and robust way to implement assignment that handles self-assignment and provides strong exception safety. Taking the parameter by value is a key part of this idiom.
2. The Quest for Performance: Move Semantics
Our class is now correct, but is it efficient? A deep copy involves a potentially slow heap allocation and a loop to copy every element. This is acceptable when we truly need an independent copy. But what about cases where the source object is a temporary that's about to be destroyed anyway? For example:
std::vector<DynamicArray> my_arrays;my_arrays.push_back(DynamicArray()); // Creates a temporary DynamicArray
In this case, creating a full copy only to have the original temporary immediately destroyed is wasteful. Modern C++ provides a much better way: move semantics.
Instead of copying the resource, we can "move" it. This means the new object "steals" the pointer to the heap-allocated data from the temporary source object, and then sets the source object's pointer to nullptr. This avoids a new allocation and a data copy entirely. It's an incredibly fast, constant-time operation.

To enable this, we add two more special member functions to our class:
- Move Constructor:
DynamicArray(DynamicArray&& other) noexcept - Move Assignment Operator:
operator=(DynamicArray&& other) noexcept
These, combined with the original three, form the Rule of Five. The && signifies an "rvalue reference," which is a reference to a temporary or movable object. The noexcept is crucial for performance, as it guarantees to the standard library containers (like std::vector) that your move operations won't throw exceptions, allowing them to use moves instead of falling back to slower copies during operations like resizing.
To see the dramatic performance impact, watch this demonstration.
Classes part 10 - Rule of Five - Have fun reducing memory allocations! | Modern Cpp Series Ep. 46
In this video, "Rule of Five," Mike Shah demonstrates the performance difference between copying and moving objects into a std::vector.
Watch the section from this timestamp. He uses valgrind to count memory allocations. Pay close attention to the number of allocations with push_back(temp) (which copies) versus push_back(std::move(temp)) (which moves). The reduction in allocations is the key takeaway.
3. Unifying the Rules: The Rule of Five
Why do we need a "Rule of Five"? Why not just add a move constructor when we feel like it? The reason is subtle but critical.
If you define any of the copy functions (copy constructor, copy assignment) or a destructor, the compiler will not automatically generate the move functions for you.
This means that if you implement the Rule of Three to make your class copyable, you silently disable move semantics. Any code that could have moved your object will now fall back to making a slow copy. Your code will be correct, but your performance will suffer silently.
Therefore, the modern C++ guideline is:
If you define or
=deleteany of the five special member functions (Destructor, Copy Constructor, Copy Assignment, Move Constructor, Move Assignment), you should define or=deleteall of them.
The following chart illustrates the complex rules the compiler follows for generating these functions. The Rule of Five simplifies this by forcing you to be explicit about your class's behavior.

4. Project: Upgrading DynamicArray to Follow the Rule of Five
It's time to apply this knowledge. You will now upgrade the DynamicArray class you created in the last lesson to be fully compliant with the Rule of Five. You'll add a correct copy constructor, copy assignment operator, move constructor, and move assignment operator.
The following article provides a complete, commented example of a class that implements the Rule of Five. Use it as a detailed reference for your implementation.
This article provides a full implementation of a MovableClass that correctly follows the Rule of Five. The code is broken down into numbered comments, which you can use as a guide for your own DynamicArray.
Study the complete class definition in Listing 17. Pay close attention to the implementation of: (4) The copy constructor: It performs a deep copy. (5) The move constructor: It "steals" the resource and nullifies the source. (6) The copy assignment operator: Note the check for self-assignment. A more robust implementation would use the copy-and-swap idiom mentioned earlier. (7) The move assignment operator: It releases its own resource before taking ownership of the source's resource.
Your Task:
Modify your DynamicArray.h and associated implementation file. Your goal is to add the four missing special functions to your existing DynamicArray<T> template class. Add print statements (e.g., std::cout << "Copy constructor called\n";) to each of the five special functions so you can trace their execution.
-
Copy Constructor (
DynamicArray(const DynamicArray& other))- Perform a deep copy.
- Allocate a new array of the same capacity as
other. - Copy
m_Sizeandm_Capacity. - Use a loop or
std::memcpyto copy the elements fromother.m_Datato the newly allocatedm_Data.
-
Copy Assignment Operator (
operator=(const DynamicArray& other))- Protect against self-assignment (e.g.,
if (this == &other) return *this;). - Free the existing memory held by
this->m_Data. - Perform the same deep copy logic as the copy constructor.
- Return a reference to the current object (
*this).
- Protect against self-assignment (e.g.,
-
Move Constructor (
DynamicArray(DynamicArray&& other) noexcept)- "Steal" the resources from
other. Copyother.m_Data,other.m_Size, andother.m_Capacitytothis. - Leave the
otherobject in a valid but empty state. Setother.m_Data = nullptr;andother.m_Size = other.m_Capacity = 0;. This preventsother's destructor from freeing the memory you just moved. - Remember the
noexceptkeyword.
- "Steal" the resources from
-
Move Assignment Operator (
operator=(DynamicArray&& other) noexcept)- Protect against self-assignment.
- Free the existing memory held by
this->m_Data. - Perform the same resource-stealing logic as the move constructor.
- Return a reference to
*this.
-
Test Your Implementation
- In
main.cpp, write test cases that explicitly trigger each of your new functions. Use your print statements to verify that the correct function is being called.
// Example test cases DynamicArray<int> A; A.push_back(1); A.push_back(2); // Test Copy Constructor std::cout << "Testing Copy Constructor...\n"; DynamicArray<int> B = A; // Test Copy Assignment std::cout << "\nTesting Copy Assignment...\n"; DynamicArray<int> C; C = A; // Test Move Constructor std::cout << "\nTesting Move Constructor...\n"; DynamicArray<int> D = std::move(A); // After this, 'A' should be in a valid but empty state. // Test Move Assignment std::cout << "\nTesting Move Assignment...\n"; DynamicArray<int> E; E = std::move(D); - In
Conclusion
Congratulations on building a truly robust, resource-managing class. You've gone from a simple class with a memory leak to a fully-featured container that handles copying and moving with both correctness and efficiency.
Key Takeaways:
- The default compiler-generated copy for a class with raw pointers performs a shallow copy, leading to double-free errors.
- The Rule of Three (Destructor, Copy Constructor, Copy Assignment) solves the correctness problem by enabling deep copies.
- Move semantics (Move Constructor, Move Assignment) solves the performance problem by "stealing" resources from temporary objects instead of copying them.
- The Rule of Five unifies these concepts, ensuring your class has a complete and explicit policy for all resource management operations.
Writing these five functions correctly is a significant amount of work and can be error-prone. This leads to a natural question: can we do better? In our next lesson, we will explore C++'s smart pointers, std::unique_ptr and std::shared_ptr. These are library classes that wrap raw pointers and implement the Rule of Five for you, often allowing you to write much simpler classes that follow the "Rule of Zero"—defining none of the special members yourself.