58 Intermediate The Hundred
The Copy That Freed Your Memory Twice
one new[], one owner, and somehow two funerals
#include <cstring>
#include <iostream>
struct Buffer {
char* data;
Buffer(const char* s) : data(new char[std::strlen(s) + 1]) { std::strcpy(data, s); }
~Buffer() {
std::cout << "freeing " << static_cast<void*>(data) << std::endl;
delete[] data;
}
};
void show(Buffer b) { std::cout << "show sees: " << b.data << std::endl; }
int main() {
Buffer greeting("hello, world");
show(greeting); // just printing it, nothing gets modified
}
Run it. There is one new[] — how many frees do you expect?
Answer
Two, at the same address — and the second one kills the process. Typical output (GCC 11 / glibc on x86-64 Linux; the address varies run to run, the repeat does not):
Why¶
Buffer never says how it should be copied, so the compiler writes that constructor for it —
memberwise: for a lone char* that copies the address, not the bytes. show's by-value
parameter is a second Buffer aimed at the same block; it destructs first, then greeting
frees the block again. That second delete[] is undefined behavior — glibc's tcache
catches it here, but it could as easily corrupt the heap quietly or appear to work.
-Wall -Wextra say nothing here; GCC and Clang both warn under -Wdeprecated-copy-dtor.
The fix¶
struct Buffer { // no destructor, no copy operations, nothing to get wrong
std::string data; // copies and frees itself (a std::vector member would too)
Buffer(const char* s) : data(s) {} // without it, C++17 needs Buffer{"hello, world"}
};
That's the Rule of Zero. Own raw memory and the Rule of Three binds: a destructor obliges a copy constructor and copy assignment too — plus the two moves since C++11, so Five.
Takeaway: if your destructor releases something, the default copy constructor is a bug.
Try it: g++ -std=c++17 main.cpp -o demo && ./demo