I’ve been working on a C++ project (blame Qt), and I recently stumbled across an issue that seemed to be caused by not following the Rule of 3/5: after ‘reconstructing’ an object by assigning a newly-constructed temporary object to it, my program began crashing with some kind of use-after-free error.
I decided to do some research, which sent me down the rabbit hole that is copy constructors, copy assignment operators, move constructors, and move assignment operators.
After I picked my jaw up off the floor, I set about adding these to one of my classes. Unfortunately, the code was quite large, verbose, and full of duplication:
SpriteMappings::SpriteMappings(const SpriteMappings &other)
: frames(other.frames)
, pieces(other.pieces)
{
this->frames = new Frame[this->total_frames];
std::copy(other.frames, other.frames + other.total_frames, this->frames);
this->pieces = new Piece[this->total_pieces];
std::copy(other.pieces, other.pieces + other.total_pieces, this->pieces);
}
SpriteMappings::SpriteMappings(SpriteMappings &&other)
: total_frames(other.total_frames)
, total_pieces(other.total_pieces)
{
this->frames = other.frames;
this->pieces = other.pieces;
other.frames = nullptr;
other.total_frames = 0;
other.pieces = nullptr;
other.total_pieces = 0;
}
SpriteMappings& SpriteMappings::operator=(const SpriteMappings &other)
{
if (this->total_frames != other.total_frames)
{
this->total_frames = other.total_frames;
delete[] this->frames;
this->frames = new Frame[this->total_frames];
}
std::copy(other.frames, other.frames + other.total_frames, this->frames);
if (this->total_pieces != other.total_pieces)
{
this->total_pieces = other.total_pieces;
delete[] this->pieces;
this->pieces = new Piece[this->total_pieces];
}
std::copy(other.pieces, other.pieces + other.total_pieces, this->pieces);
return *this;
}
SpriteMappings& SpriteMappings::operator=(SpriteMappings &&other)
{
this->total_frames = other.total_frames;
delete[] this->frames;
this->frames = other.frames;
this->total_pieces = other.total_pieces;
delete[] this->pieces;
this->pieces = other.pieces;
other.frames = nullptr;
other.total_frames = 0;
other.pieces = nullptr;
other.total_pieces = 0;
return *this;
}
SpriteMappings::~SpriteMappings()
{
delete[] this->frames;
delete[] this->pieces;
}
I didn’t like this, especially since a copy/move constructor and its corresponding assignment operator seemed to mostly do the same thing – could these not share code somehow?
A method I found that did allow a constructor and assignment operator to share code was the ‘copy and swap idiom‘. Not only that, but it also allowed copy constructors/operators to share code with move constructors/operators. This code compactness seemed great, but I didn’t like that the process of swapping required a third, temporary object. Considering that my objects were responsible for large buffers, this seemed like an awful waste of RAM.
The code that I’d written had a lot of duplication: the code used to copy/move each of the object’s buffers was exactly the same. This had me wondering if could make a buffer class that would allow me to make both buffers share their copy/move code. But, wait, doesn’t C++ already have a bunch of container classes that do that? After giving it some thought, I settled on replacing my class’s buffers with vector
s, and, as a result, I was able to greatly simplify the constructors and assignment operators:
SpriteMappings::SpriteMappings(const SpriteMappings &other)
: frames(other.frames)
, pieces(other.pieces)
{
}
SpriteMappings::SpriteMappings(SpriteMappings &&other)
: frames(std::move(other.frames))
, pieces(std::move(other.pieces))
{
}
SpriteMappings& SpriteMappings::operator=(const SpriteMappings &other)
{
this->frames = other.frames;
this->pieces = other.pieces;
return *this;
}
SpriteMappings& SpriteMappings::operator=(SpriteMappings &&other)
{
this->frames = std::move(other.frames);
this->pieces = std::move(other.pieces);
return *this;
}
SpriteMappings::~SpriteMappings()
{
}
My, it’s so minimal! It’s so sleek! It’s so efficient that the destructor doesn’t need any code!
Wait… the destructor doesn’t need any code?
The Rule of 3/5 says that if you need a copy/move constructor, copy/move assignment operator, or a destructor, then you probably need all of them. But clearly I don’t actually need an explicit destructor anymore, as the default implicit one will do the job just fine!
Actually… now that I think about it, all of those methods can be replaced with their defaults.
After doing that, here’s my code:
// Haha
That’s right: there isn’t any! By using the proper containers, I don’t need explicit copy/move constructors, copy/move assignment operators, or even a destructor anymore! The code’s practically writing itself!