遵循Rule of Three:为何删除拷贝构造/赋值之一留隐式定义易出错?
Great question! Let's break down why this mistake leads to bugs, using concrete examples to make it clear.
Background
Classes that manage non-copyable resources (like file handles, network connections, or hardware access) rely on a "handle" (e.g., a pointer or file descriptor) to interact with the resource. The resource itself can't be duplicated—you can't make an independent copy of an open file handle that points to the same file state, for instance. So the class's copy semantics need to align with this: either disable copying entirely, or implement meaningful deep copy (which is often impossible or undesirable for these resources).
Why Partial Deletion Breaks Things
If you delete only one copy operation (say, the copy constructor) but leave the other (copy assignment) to be implicitly generated, the compiler creates a shallow copy for the remaining operation. A shallow copy just copies the handle's value (like a pointer address) instead of duplicating the underlying resource. This leads to two critical issues:
- Double-free/resource corruption: When both objects go out of scope, they'll both try to release the same resource.
- Unexpected shared state: Changes to the resource via one object will affect the other, since they share the same handle.
Example 1: Deleting Copy Constructor but Keeping Implicit Copy Assignment
#include <cstdio> #include <iostream> class FileHandler { private: FILE* file; public: // Open a file on construction FileHandler(const char* filename) : file(std::fopen(filename, "w")) { if (!file) std::cerr << "Failed to open file!\n"; } // Close the file on destruction ~FileHandler() { if (file) { std::fclose(file); std::cout << "Closed file handle\n"; } } // Delete copy constructor, but leave copy assignment implicit FileHandler(const FileHandler&) = delete; // Write content to the file void write(const char* msg) { if (file) std::fputs(msg, file); } }; int main() { FileHandler f1("test.txt"); FileHandler f2("temp.txt"); f2 = f1; // Implicit copy assignment does a shallow copy of the FILE* f1.write("Hello from f1\n"); f2.write("Hello from f2\n"); // When main exits: // 1. f2 is destroyed first, closing the file handle // 2. f1 is destroyed next, trying to close the already closed handle // This is UNDEFINED BEHAVIOR (crash, corruption, or silent failure) return 0; }
What's Wrong Here?
- The implicit copy assignment copies the
FILE*pointer fromf1tof2, so both objects now point to the same open file. f2's original file handle (fortemp.txt) is overwritten without being closed—this is a resource leak.- When the program ends, both
f1andf2try to close the same file handle. Closing an already closed descriptor is undefined behavior, which can crash your program or cause system-level issues.
Example 2: Deleting Copy Assignment but Keeping Implicit Copy Constructor
#include <cstdio> #include <iostream> class FileHandler { private: FILE* file; public: FileHandler(const char* filename) : file(std::fopen(filename, "w")) { if (!file) std::cerr << "Failed to open file!\n"; } ~FileHandler() { if (file) { std::fclose(file); std::cout << "Closed file handle\n"; } } // Delete copy assignment, but leave copy constructor implicit FileHandler& operator=(const FileHandler&) = delete; void write(const char* msg) { if (file) std::fputs(msg, file); } }; int main() { FileHandler f1("test.txt"); FileHandler f2 = f1; // Implicit copy constructor does a shallow copy f1.write("Hello from f1\n"); f2.write("Hello from f2\n"); // Again, both objects will try to close the same handle on destruction return 0; }
What's Wrong Here?
- The implicit copy constructor creates
f2as a shallow copy off1, so both share the same file handle. - When
mainexits, bothf1andf2destroy the same resource, leading to the same double-free undefined behavior as before. - Any writes to either object affect the same file, which is almost never what you intend when copying a resource handler.
The Fix: Disable Both Copy Operations (or Implement Proper Semantics)
For classes managing non-copyable resources, the correct approach is to disable both the copy constructor and copy assignment operator (using = delete), or implement meaningful copy semantics if possible (though for most non-copyable resources, deep copy isn't feasible).
Here's the corrected version of the FileHandler class:
#include <cstdio> #include <iostream> class FileHandler { private: FILE* file; public: FileHandler(const char* filename) : file(std::fopen(filename, "w")) { if (!file) std::cerr << "Failed to open file!\n"; } ~FileHandler() { if (file) { std::fclose(file); std::cout << "Closed file handle\n"; } } // Disable both copy operations FileHandler(const FileHandler&) = delete; FileHandler& operator=(const FileHandler&) = delete; // Optional: Enable move semantics if you want to transfer ownership FileHandler(FileHandler&&) noexcept = default; FileHandler& operator=(FileHandler&&) noexcept = default; void write(const char* msg) { if (file) std::fputs(msg, file); } };
This way, you can't accidentally create copies of the handler, which prevents resource sharing and double-free issues entirely.
内容的提问来源于stack exchange,提问作者sunshilong369

