Reputation: 167
I have a class with own resource management:
class Lol
{
private:
// This is data which this class allocates
char *mName = nullptr;
public:
Lol(std::string str) // In constructor just copy data from string
{
auto cstr = str.c_str();
auto len = strlen(cstr);
mName = new char[len + 1];
strcpy(mName, cstr);
};
~Lol() // And of course clean up
{
delete[] mName;
}
}
I implemented copy constructor which just copies managed data:
Lol(const Lol &other)
{
auto cstr = other.mName;
auto len = strlen(cstr);
mName = new char[len + 1];
strcpy(mName, cstr);
};
I also need to implement copy assignment operator. I just did this:
Lol &operator=(const Lol &other)
{
if (this == &other)
{
return *this;
}
// Clean up my resources
this->~Lol();
// And copy resources from "other" using already implemented copy constructor
new (this) Lol(other);
}
Looks like this copy assignment operator will work for all classes. Why do I need to have another code in the copy assignment operator? What is use case for it?
Upvotes: 1
Views: 179
Reputation: 264639
Use the copy and swap Idiom.
Lol &operator=(const Lol &other)
{
if (this == &other)
{
return *this;
}
// Clean up my resources
this->~Lol();
// Copy can throw.
// Then your object is in an undefined state.
new (this) Lol(other);
// You forgot the return:
return *this;
}
So this does not provide the strong (or any) exception gurantees.
The preferred way would be:
Lol& operator=(Lol const& other)
{
Lol copy(other); // Here we use the copy constructor
// And the destructor at the end of
// function cleans up the scope
// Note this happens after the swap
// so you are cleaning up what was in
// this object.
swap(copy);
return *this;
}
void swap(Lol& other) noexcept
{
std::swap(mName, other.mName);
}
Nowadays we have improved on this original
Lol& operator=(Lol copy) // Notice we have moved the copy here.
{
swap(copy);
return *this;
}
The exciting thing here is that this version of assignment works for both copy and move assignment just as effeciently.
Upvotes: 1
Reputation: 96791
If the constructor throws, you'd have to catch the exception and somehow recover (by calling some constructor, probably by the default one), which makes this much less elegant. Failing to call a constructor will lead to a double destruction and UB.
Also this approach leads to UB if you inherit from such class, or use it as a [[no_unique_address]]
member variable:
An object o1 is transparently replaceable by an object o2 if: ...
— neither o1 nor o2 is a potentially-overlapping subobject ...
and [intro.object]/7
A potentially-overlapping subobject is either:
— a base class subobject, or
— a non-static data member declared with the no_unique_address attribute.
This is not UB per se, but if your object isn't transparently replaceable, the reconstructed object must be std::launder
ed before use, which isn't practical (e.g. if it's an automatic variable, the automatic destruction will happen without std::launder
→ UB).
C++17 had even more restrictions. You also needed std::launder
if your class contained const or reference members (C++17 [basic.life]/8.3
).
If you're looking for an universal assignment operator, there is one. It's called the copy&swap idiom. Behold:
MyClass &operator=(MyClass other) noexcept
{
std::swap(x, other.x); // Swap every member here.
return *this;
}
This acts as both copy and move assignment (if you have the respective constructors), and offers the strong exception guarantee (if the copy throws, the target object is unchanged).
The only case (that I know) where it doesn't work out of the box is when the class maintains a pointer to itself somewhere (possibly inside itself).
Upvotes: 2