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 {
float *data;
public:
HeapFloat(float f) : data(new float(f)) { }
~HeapFloat() { delete data; }
float get() const { return *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 {
float *data;
public:
HeapFloat(float f) : data(new float(f)) { }
~HeapFloat() { delete data; }
float get() const { return *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 {
float *data;
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; }
};
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 {
float *data;
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; }
};
HeapFloat a(1.2), b(3.4);
a = b;
cout << a.get() << ' ' << b.get() << endl;
c.cc:11: warning: implicitly-declared ‘constexpr main()::HeapFloat&
main()::HeapFloat::operator=(const main()::HeapFloat&)’ is deprecated
c.cc:5: note: because ‘main()::HeapFloat’ has user-provided
‘main()::HeapFloat::HeapFloat(const main()::HeapFloat&)’
3.4 3.4
free(): double free detected in tcache 2
SIGABRT: Aborted
- The default assignment operator has the same problem.
- Also, relying upon the compiler-created
operator=
when you’ve
written the copy ctor is deprecated.
Fix Assignment Operator
class HeapFloat {
float *data;
public:
HeapFloat(float f) : data(new float(f)) { }
HeapFloat(const HeapFloat &rhs) : data(new float(*rhs.data)) { }
HeapFloat& operator=(const HeapFloat &rhs) {
delete data;
data = new float(*rhs.data);
return *this;
}
~HeapFloat() { delete data; }
float get() const { return *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 {
float *data;
public:
HeapFloat(float f) : data(new float(f)) { }
HeapFloat(const HeapFloat &rhs) : data(new float(*rhs.data)) { }
HeapFloat& operator=(const HeapFloat &rhs) {
delete data;
data = new float(*rhs.data);
return *this;
}
~HeapFloat() { delete data; }
float get() const { return *data; }
};
HeapFloat a(1.2);
a = a;
cout << a.get() << '\n';
2.75495e-42
Explanation
class HeapFloat {
float *data;
public:
HeapFloat(float f) : data(new float(f)) { }
HeapFloat(const HeapFloat &rhs) : data(new float(*rhs.data)) { }
HeapFloat& operator=(const HeapFloat &rhs) {
delete data;
data = new float(*rhs.data);
return *this;
}
~HeapFloat() { delete data; }
float get() const { return *data; }
};
HeapFloat a(1.2);
a = a;
cout << a.get() << '\n';
1.1827e-41
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 {
float *data;
public:
HeapFloat(float f) : data(new float(f)) { }
HeapFloat(const HeapFloat &rhs) : data(new float(*rhs.data)) { }
HeapFloat& operator=(const HeapFloat &rhs) {
if (this != &rhs) { // avoid self-assignment
delete data;
data = new float(*rhs.data);
}
return *this;
}
~HeapFloat() { delete data; }
float get() const { return *data; }
};
HeapFloat a(1.2);
a = a;
cout << a.get() << '\n';
1.2
Explanation
HeapFloat& operator=(const HeapFloat &rhs) {
if (this != &rhs) {
delete data;
data = new float(*rhs.data);
}
return *this;
}
- 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”.
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 {
float *data;
public:
HeapFloat(float f) : data(new float(f)) { }
HeapFloat(const HeapFloat &rhs) : data(new float(*rhs.data)) { }
HeapFloat& operator=(const HeapFloat &rhs) {
if (this != &rhs) { // avoid self-assignment
delete data;
data = new float(*rhs.data);
}
return *this;
}
~HeapFloat() { delete data; }
float get() const { return *data; }
};
HeapFloat a(1.2);
a = a;
cout << a.get() << '\n';
1.2
DRY
class HeapFloat {
float *data = nullptr;
public:
HeapFloat(float f) : data(new float(f)) { }
HeapFloat(const HeapFloat &rhs) { *this = rhs; }
HeapFloat& operator=(const HeapFloat &rhs) {
if (this != &rhs) { // avoid self-assignment
delete data;
data = new float(*rhs.data);
}
return *this;
}
~HeapFloat() { delete data; }
float get() const { return *data; }
};
HeapFloat a(1.2);
a = a;
cout << a.get() << '\n';
1.2
The copy ctor calls operator=
; as complex objects often do.
Initialize data = nullptr
so the initial delete data
is ok.