The Rule of Three/Five and SDL Resource Management
What is the "Rule of Three" and why did deleting the copy operations satisfy it here? What about the Rule of Five?
The Rule of Three is a guideline in C++ that states: if a class requires a user-defined destructor, a user-defined copy constructor, OR a user-defined copy assignment operator, it almost certainly needs all three.
Why? The need for any one of these typically implies the class is managing a resource manually (like raw memory, a file handle, a network socket, or in our case, an SDL_Window*
).
- Destructor (
~Window()
): Needed to release the resource (e.g.,SDL_DestroyWindow(SDLWindow)
). - Copy Constructor (
Window(const Window&)
): The default compiler-generated copy constructor performs a member-wise copy. For a pointer likeSDLWindow
, this means both the original object and the new copy would end up pointing to the sameSDL_Window
. When one object's destructor runs (SDL_DestroyWindow
), the other object is left with a dangling pointer. If the second object's destructor runs later, it tries to destroy the same window again, leading to a crash (double-free). A user-defined copy constructor would need to handle this, perhaps by creating a newSDL_Window
for the copy (deep copy) or by disallowing copying. - Copy Assignment Operator (
operator=(const Window&)
): Similar issues arise with the default assignment operator. Assigning oneWindow
to another would copy the pointer, leading to the same double-free or dangling pointer problems and potential self-assignment issues. A user-defined version needs to handle resource cleanup and proper copying.
In our Window
class:
- We needed a destructor
~Window()
to callSDL_DestroyWindow(SDLWindow)
. - This signals manual resource management, invoking the Rule of Three.
- We decided that copying
Window
objects doesn't make sense or is complex (creating a duplicate actual window isn't usually desired). - Therefore, instead of writing complex copy operations, we explicitly deleted them using
= delete;
.
class Window {
public:
// Constructor acquires resource
Window() {
SDLWindow = SDL_CreateWindow(/* ... */);
}
// Destructor releases resource
~Window() {
if (SDLWindow) { // Good practice: check if null
SDL_DestroyWindow(SDLWindow);
}
}
// Rule of Three: Because we defined a destructor,
// we must consider copy operations.
// We choose to disallow copying:
Window(const Window&) = delete;
Window& operator=(const Window&) = delete;
private:
SDL_Window* SDLWindow{nullptr};
};
Deleting the copy operations satisfies the Rule of Three by explicitly addressing the copy behavior (by forbidding it), thus preventing the dangerous default shallow copies.
The Rule of Five (C++11 and Later)
C++11 introduced move semantics (move constructor and move assignment operator) as a way to efficiently transfer resource ownership without expensive copying. This expanded the guideline to the Rule of Five: if a class defines any of the destructor, copy constructor, copy assignment operator, move constructor, or move assignment operator, it should likely define or delete/default all five.
- Move Constructor (
Window(Window&&)
): Transfers ownership of the resource (theSDL_Window*
) from a temporary or expiring object to a new object. - Move Assignment Operator (
operator=(Window&&)
): Transfers ownership of the resource to an existing object, properly releasing the destination's old resource first.
For our Window
class, since we deleted the copy operations, the compiler might implicitly delete the move operations too (depending on the compiler and context). However, it's best practice under the Rule of Five to be explicit if you define a destructor:
class Window {
public:
// Constructor acquires resource
Window() { /* ... */ }
// Destructor releases resource
~Window() { /* ... */ }
// Rule of Five: Address all five special members
// Disallow copying
Window(const Window&) = delete;
Window& operator=(const Window&) = delete;
// Decide on move semantics (e.g., default them,
// delete them, or implement them)
Window(Window&&) = default;
Window& operator=(Window&&) = default;
private:
SDL_Window* SDLWindow{nullptr};
};
In this specific case, deleting copies is sufficient to prevent the primary resource management errors. Defaulting or implementing move semantics could be done but adds complexity. Deleting copy operations is the simplest, safest approach when copying is not required.
Copy Constructors and Operators
Explore advanced techniques for managing object copying and resource allocation
Move Semantics
Learn how we can improve the performance of our types using move constructors, move assignment operators and std::move()
Creating a Window
Learn how to create and customize windows, covering initialization, window management, and rendering