Show Lecture.Self-assignment as a slide show.
CS253 Self-assignment
Intro
This illustrates the danger of self-assignment.
That is, when an object containing pointers or other handles to external
resources is assigned to itself, you’ve got to be careful in
operator=
, or you might destroy your data before you copy it to
yourself.
Copy Constructor General Outline
When a class contains a handle to external data, the copy constructor
generally follows this pattern:
- Copy the resource from the other object to this object.
That is, don’t just copy the handle (pointer, file descriptor,
etc.). Instead you have to make an actual copy of the resource
(copy the memory from new, create a new temporary file and copy
the contents, etc.).
Of course, this is a copy constructor, not an assignment operator,
so there was no previous value. We are constructing.
Assignment Operator General Outline
When a class contains a handle to external data, the assignment
operator (operator=
) generally follows this pattern:
- Get rid of the old external data (delete memory,
close network socket, delete temporary file, unlock semaphore, etc.).
- Copy the resource from the other object to this object.
Code
class HeapFloat {
public:
HeapFloat(float f) : data(new float(f)) { }
~HeapFloat() { delete[] data; }
float get() const { return *data; }
float *data;
};
HeapFloat a(1.2);
cout << a.get();
1.2
- This is a silly class that stores a float in dynamic memory.
- It works—so far.
Try Copy Constructor
class HeapFloat {
public:
HeapFloat(float f) : data(new float(f)) { }
~HeapFloat() { delete[] data; }
float get() const { return *data; }
float *data;
};
HeapFloat a(1.2), b(a);
cout << a.get() << ' ' << b.get();
free(): double free detected in tcache 2
SIGABRT: Aborted
- That failed because the default copy ctor just copied the pointer.
- Both objects’ dtors tried to free the same pointer … boom!
- We deserve that, because we didn’t follow the general outline
for a copy constructor. We got the default copy ctor.
Fixed Copy Constructor
class HeapFloat {
public:
HeapFloat(float f) : data(new float(f)) { }
HeapFloat(const HeapFloat &rhs) : data(new float(*rhs.data)) { }
~HeapFloat() { delete[] data; }
float get() const { return *data; }
float *data;
};
HeapFloat a(1.2), b(a);
cout << a.get() << ' ' << b.get();
1.2 1.2
- The new copy ctor allocated more space and copied the data,
not just the pointer.
Try Assignment Operator
class HeapFloat {
public:
HeapFloat(float f) : data(new float(f)) { }
HeapFloat(const HeapFloat &rhs) : data(new float(*rhs.data)) { }
~HeapFloat() { delete[] data; }
float get() const { return *data; }
float *data;
};
HeapFloat a(1.2), b(3.4);
a = b;
cout << a.get() << ' ' << b.get();
free(): double free detected in tcache 2
SIGABRT: Aborted
- The default assignment operator has the same problem.
Fix Assignment Operator
class HeapFloat {
public:
HeapFloat(float f) : data(new float(f)) { }
HeapFloat(const HeapFloat &rhs) : data(new float(*rhs.data)) { }
void operator=(const HeapFloat &rhs) {
delete data;
data = new float(*rhs.data);
}
~HeapFloat() { delete[] data; }
float get() const { return *data; }
float *data;
};
HeapFloat a(1.2), b(3.4);
a = b;
cout << a.get() << ' ' << b.get();
3.4 3.4
- The assignment operator now allocates space & copies the data.
Self-assignment
class HeapFloat {
public:
HeapFloat(float f) : data(new float(f)) { }
HeapFloat(const HeapFloat &rhs) : data(new float(*rhs.data)) { }
void operator=(const HeapFloat &rhs) {
delete data;
data = new float(*rhs.data);
}
~HeapFloat() { delete[] data; }
float get() const { return *data; }
float *data;
};
HeapFloat a(1.2);
a = a;
cout << a.get() << '\n';
8.25925e-42
Explanation
class HeapFloat {
public:
HeapFloat(float f) : data(new float(f)) { }
HeapFloat(const HeapFloat &rhs) : data(new float(*rhs.data)) { }
void operator=(const HeapFloat &rhs) {
delete data;
data = new float(*rhs.data);
}
~HeapFloat() { delete[] data; }
float get() const { return *data; }
float *data;
};
HeapFloat a(1.2);
a = a;
cout << a.get() << '\n';
2.66947e-42
What happens inside a = a
?
- First, delete the old data.
- Second, copy the new data (that we just freed).
- Uh-oh!
- We must copy the data, but, sometimes, we can’t !
Fix
class HeapFloat {
public:
HeapFloat(float f) : data(new float(f)) { }
HeapFloat(const HeapFloat &rhs) : data(new float(*rhs.data)) { }
void operator=(const HeapFloat &rhs) {
if (this != &rhs) { // avoid self-assignment
delete data;
data = new float(*rhs.data);
}
}
~HeapFloat() { delete[] data; }
float get() const { return *data; }
float *data;
};
HeapFloat a(1.2);
a = a;
cout << a.get() << '\n';
1.2
Victory!
Explanation
void operator=(const HeapFloat &rhs) {
if (this != &rhs) {
delete data;
data = new float(*rhs.data);
}
}
- If this is self-assignment, don’t do anything.
- How to detect that?
- See if the address of the left-hand-side (this)
equals the address of the right-hand-side (
&rhs
).
- The
&
in the declaration const HeapFloat &rhs
means
“reference to”, whereas the &
in the expression
this != &rhs
means ”address of”.
operator=
usually returns a reference to the current object
via *this
, to allow chain assignment.
This Sounds Silly
- Yeah, but, … so what? Nobody will ever really do that.
- Not explicitly, no. But it might happen indirectly.
- Imagine that you’re sorting a vector:
- Find the smallest element.
- Swap it with the first element.
- What if the first element is the smallest element?
- You’ll try to assign
[0]
to [0]
! Self-assignment!
Not very DRY
class HeapFloat {
public:
HeapFloat(float f) : data(new float(f)) { }
HeapFloat(const HeapFloat &rhs) : data(new float(*rhs.data)) { }
void operator=(const HeapFloat &rhs) {
if (this != &rhs) { // avoid self-assignment
delete data;
data = new float(*rhs.data);
}
}
~HeapFloat() { delete[] data; }
float get() const { return *data; }
float *data;
};
HeapFloat a(1.2);
a = a;
cout << a.get() << '\n';
1.2
new float(*rhs.data)
appears twice—WET!
DRY
class HeapFloat {
public:
HeapFloat(float f) : data(new float(f)) { }
HeapFloat(const HeapFloat &rhs) { *this = rhs; }
void operator=(const HeapFloat &rhs) {
if (this != &rhs) { // avoid self-assignment
delete data;
data = new float(*rhs.data);
}
}
~HeapFloat() { delete[] data; }
float get() const { return *data; }
float *data = nullptr;
};
HeapFloat a(1.2);
a = a;
cout << a.get() << '\n';
1.2
Now DRY. The copy ctor invokes the
assignment operator; complex objects often do this. Initialize
data = nullptr
so the initial delete data
is ok.