The writing to another, temporary workspace is not really negotiable in terms of exception safety.
Take this for example:
Code:
Screen & Screen::operator=(const Screen &rhs){
if (this != &rhs){
delete[] backBuffer;
delete[] frontBuffer;
backBuffer = new unsigned char[rhs.bufferSize];
frontBuffer = new unsigned char[rhs.bufferSize];
memcpy(backBuffer,rhs.backBuffer,rhs.bufferSize);
memcpy(frontBuffer,rhs.frontBuffer,rhs.bufferSize);
bufferSize = rhs.bufferSize;
}
}
This has no exception safety at all. What if "backBuffer = new unsigned char[rhs.bufferSize];" throws an exception? Not only will the object be left with the assignment only partially complete, the object is left in a terrible state: it can't even be destroyed! backBuffer and frontBuffer still point to memory that has already been freed, so calling delete[] on them would instantly crash the program.
Adding code in between the delete[] and the new[] to set both pointers to NULL would get rid of that problem, so let's suppose we do that:
Code:
Screen & Screen::operator=(const Screen &rhs){
if (this != &rhs){
delete[] backBuffer;
delete[] frontBuffer;
backBuffer = NULL;
frontBuffer = NULL;
backBuffer = new unsigned char[rhs.bufferSize];
frontBuffer = new unsigned char[rhs.bufferSize];
memcpy(backBuffer,rhs.backBuffer,rhs.bufferSize);
memcpy(frontBuffer,rhs.frontBuffer,rhs.bufferSize);
bufferSize = rhs.bufferSize;
}
}
Now, assume that "frontBuffer = new unsigned char[rhs.bufferSize];" throws an exception. The object is in an "impossible" state. backBuffer points to memory of a certain size (the size of rhs.bufferSize), frontBuffer is a NULL pointer, and bufferSize is meaningless because it was never updated. The object can be safely destroyed, but it can probably never again be used. This has the lowest level of exception safety: it doesn't leak memory in the face of an exception. However, when an exception happens, the object is left in an intermediate state; a state that is neither the initial nor the final state, and potentially not even a legal state.
Copy and swap, or a functionally equivalent method, is the only way to get strong exception safety. Strong exception safety states that if an exception is thrown in a function, it will be as if the function never happened. That is, if the code succeeds, the object will be in its final state; if the code fails, the object's state is unaltered.
In the following code:
Code:
Screen & Screen::operator=(Screen rhs){
swap(rhs);
}
the only thing that can fail is the copy construction. If copy construction throws, then the swap will never take place, which guarantees our object remains fully unaltered. If copy construction succeeds, we know we succeed fully, because we have coded swap such that it cannot throw, and the destructor also cannot throw for any well-written class.
Assuming that the copy constructor can't leak memory in the face of an exception, we've achieved strong exception-safety with this function -- it either fully succeeds or it has no effect. Without making a temporary, this is impossible; the first functions, if they failed, would still have altered the object.
If we want strong exception-safety (and we should), there is no way to avoid having another copy of the data members; at the very least, all data members which can throw exceptions. And typically, in classes with large memory needs, it is dynamically allocated memory, and thus is vulnerable to throwing.
I can agree that it isn't often you make an self-assigment to an object but I find it very hard to belive that a simple if-statements(that compare a memory address) is actually slower(in CPU power) than writing to memory(small as HUGE classes). I don't have any numbers of it so I can't confirm it.
But again, making a temporary is not about performance, it's about safety, and there is no way around it (except in trivially simple cases, but trivially simple cases won't need custom assignment operators). And, once we get to the strong exception safety plateau, we also will have succeeded in making checking for self-assignment optional. Given that it's now optional, I almost always opt not to, for the reasons above.