In this topic I'll try to explain what the copy and swap idom is an what it's strenghs are.
When writting a class one is often confronted to situations where construction and destruction are non trivial. Memory managment using new and delete is probably the most common situation but not the only. Let us consider the following string class.
| CODE |
class String{ public: String(const char*str){ data = new char[strlen(str)+1]; strcpy(data, str); } ~String(){ delete[]data; } private: char*data; }; |
Obviously a lot of functionality is missing to make it useful but it is of no use in our example.
As myork pointed out in this
topic the default Copyconstructor and operator = don't do their jobs correctly. This means that we have to write them ourselfs.
| CODE |
String(const String&other){ data = new char[strlen(other.data)+1]; strcpy(data, other.data); } String&operator=(const String&other){ delete[]data; data = new char[strlen(other.data)+1]; strcpy(data, other.data); return *this; } |
(Note that I intentional did a few things wrong pointed out by myork)
One might now notice that the operator= doesn't provide any new code. The first line is identical to the destructor lines 2 to 3 are the same as in the copyconstructor and the 4th is a typical operator= line. This obviously is code dublications.
Code dublication makes it hard to change things later because at least 2 changes are need to do a single thing. So let use try to avoid this dublication.
The obvious solution would be to call the destructor and afterwards the copyconstructor. This is possible even if the syntax is not trivial.
| CODE |
String&operator=(const String&other){ ~String(); new(this) String(other); // requieres include<new> return *this; } |
This avoids the dublication but hides some serious problems. What if the copyconstructor would fail, mean throw an exception? What if we would assign the string to itself?
In the first case the result would be a zombie object which is neither alive nor dead and absolutly unusable. In the second case an access violation would be the result.
Let use first try to solve the second issue. This can be done by checking for a self assignment.
| CODE |
String&operator=(const String&other){ if(&other != this) { ~String(); new(this) String(other); } return *this; } |
This leaves use with the first problem. The only possible way to protect against this is to first create the copy before we destroy the old value. This places use before a serious problem : We construct the copy in the same place as the only string was placed, meaning we can not inverse the order because 2 objects can't live at the same time in the same place.
The only solution is to construct the copy in some other place. After it's construction we must move it to the place where the current object lives or lived. Usually one would use the operator= for this but it's rather stupid to implement the operator= in terms of itself. There is although another operation which allows similar effects than an assignment : a swap.
We create a copy somewhere in the memory, then we swap it with the current object and afterwards we can destroy the original object without a problem.
| CODE |
String&operator=(const String&other){ String temp(other); std::swap(*this, temp); return *this; // temp is implicitly destroyed } |
There are 2 major problems with the code above. First of if the swap throws, we have a zombie object. std::swap doesn't guarente not to throw. The other problem is that std::swap uses operator=. This means we have an endless recursion.
We can solve both problems by writting our own swap function.
| CODE |
void swap(String&other)throw(){ std::swap(data, other.data); } |
The throw() means that the compiler should not allow the function to throw. std::swap will not throw because the swapped pointer objects don't throw. This means our operator= would look like this
| CODE |
String&operator=(const String&other){ String temp(other); swap(temp); return *this; } |
One might now ask if the operator= is protected against self assigments. By looking a bit closer it doesn't use any operation which has twice the same operant. Meaning in the copyconstructor this != &other and in the swap methode this != &other. This means it is protected. This lets our class look like
| CODE |
class String{ public: String(const char*str){ data = new char[strlen(str)+1]; strcpy(data, str); } ~String(){ delete[]data; } String(const String&other){ data = new char[strlen(other.data)+1]; strcpy(data, other.data); } void swap(String&other)throw(){ std::swap(data, other.data); } String&operator=(const String&other){ String temp(other); swap(temp); return *this; } private: char*data; }; |
One might consider to allow assigning everything for which a copy constructor exists. This is simple using templates
| CODE |
template<class T> String&operator=(const T&other){ String temp=other; swap(temp); return *this; } |
Note that it does have a reason why I use =other and not (other). Using the first requiers that the construtor can be used for implicit conversion (meaning is not declared explicit) the second will use any constructor. An example will make this clear. Let use add a constructor which will create a string of a certain length containing only spaces.
| CODE |
| String(unsigned num){/*...*/} |
It is obviously rubbish to allow this constructor to be used in an implicit conversion so we make it explicit.
| CODE |
| explicit String(unsigned num){/*...*/} |
Making it explicit has the effect that a construction such as
is not allowed.
A good question is why it is better to write an additional methode swap instead of simply writting a operator= in the first place. There are several reasons:
- It is a lot simpler to get the swap right than the operator=.
- The operator= can be pretty much copy & pasted because it is nearly the same for each class as the swap does the actual work
- A fast swap operation is available which can be handy.
- No matter what operator= and the copy constructor behave the same way for the same input.
- By using templates the number of assignment operators doesn't grow with the number of copyconstructors but is fixed.