Re: How to check for assignment to self?
In article <roy-F60C42.19490318112007@news.panix.com>, roy@panix.com
says...
In article <G9ydnfGRdKgQ1d3anZ2dnUVZ8uWdnZ2d@eclipse.net.uk>,
Francis Glassborow <francis.glassborow@btinternet.com> wrote:
So what is a programmer supposed to do?
Learn the exception safe idiom that does not need chcking for self
assignment. The idiom you are using dates from the early 90s and has
been superceded by a new one.
Are you going to keep us in suspense, or are you going to give us a hint
what that idiom is? :-)
The idiom is to create a copy of the object being assigned, then if and
only if it's created correctly, you swap the copy with the existing
target object. This is exception safe if swap is guaranteed not to throw
(which is normally the case).
The code typically looks something like this:
class String {
char *m_string;
public:
String &operator=(String other) {
swap(other);
return *this;
// 'other', now containing the original contents of
// this string is destroyed when it goes out of scope.
}
void swap(String &other) {
char *temp = m_string;
m_string = other.m_string;
other.m_string = temp;
}
// copy ctor, dtor, etc. omitted.
};
Note that in this case we have NOT passed 'other' by reference. The
reason to pass by reference is to avoid a copy -- but in this case, we
know we're going to copy it anyway, so we can let the standard parameter
passing mechanism do that for us. Of course, if you want to make it more
explicit, you can do something like this:
// swap remains as before...
String &operator=(String const &other) {
String temp(other);
swap(temp);
return *this;
}
Either way, the basic idea is pretty simple: we never modify the
existing target item until we're sure we can do so with no chance of an
exception occurring when we do it. Anyway, we either get an exception
while we're creating the copy (in which case, the original item remains
untouched) or else no exception occurs at all, in which case our
assignment succeeds in its entirety.
Now, if creating the copy is really expensive, we CAN add a check for
self assignment as an optimization, and everything is still fine. The
problem arises with code that really _needs_ to check for the assignment
to self, such as the original code:
String& operator=( const String& other )
{
if ( &other != this )
{
delete m_string;
m_string = strdup( other.m_string );
}
return *this;
}
If this didn't check for assignment to self, when we do the 'delete
m_string', we would destroy not only the target, but also the source of
the assignment. When we attempt to duplicate the 'other', we'd read from
memory that's been deleted, and we'd get undefined behavior. Even when
we don't have self-assignment, we have a serious chance for a problem.
In the typical case, strdup looks something like:
char *strdup(char const *other) {
char *temp = malloc(strlen(other)+1);
if (temp)
strcpy(temp, other);
return temp;
}
now, if that fails, we've destroyed the original target, but we canNOT
assign the 'other' value correctly, because we don't have memory to hold
it. We no longer have the original value, but we don't have the new
value either.
Of course, with some extra work we can fix that as well, for example,
using code something like this:
String &operator=(String const &other) {
if (&other == this)
return *this;
char *temp = strdup(other);
if (temp != NULL) {
free(m_string);
m_string = temp;
}
return *this;
}
this highlights another problem with the orginal: although strdup (at
least in the implmenetations I've seen) uses malloc, the memory was
freed with delete instead of free -- leading to undefined behavior.
Anyway, this is safe if and only if we're guaranteed that 'free' never
throws an exception (usually a safe bet, since it's part of the C
library that knows nothing about exceptions), and that assigning a
pointer can't throw an exception (again, normally true).
OTOH, our code is now quite a bit longer and more complex than it
started. If we want to use new and delete to manage the memory, instead
of malloc/free as happens with strdup, our code gets longer and more
complex still:
String &operator=(String const &other) {
if (this == &other)
return *this;
char *temp = NULL;
try {
// anything that might throw an exception is in here
temp = new char[strlen(other)+1];
strcpy(temp, other.m_string);
}
catch(bad_alloc const &) {
if (temp != NULL)
delete [] temp;
return *this;
}
// down here we have only code we _know_ cannot fail.
delete [] m_string;
m_string = temp;
return *this;
}
In fairness, the part that deletes temp in the catch block is only
needed if we have a strcpy that could throw an exception. Since strcpy
is at least normally the same as in the C library, that's not normally
the case; then again, the C++ standard doesn't really guarantee it.
The code using swap is certainly quite a bit simpler and less error
prone.
--
Later,
Jerry.
The universe is a figment of its own imagination.
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]