Destruction
Why Do We Need a Destruction Phase?
We've said that in the lifecycle of objects, destruction mirrors construction. But while it's clear that variables need to be initialized before they can be used, why do we need to do anything at all when we're done with them?
The destruction phase is all about cleaning up properly after ourselves. Sometimes, there is no cleanup to do, but sometimes there is.
Suppose, for example, we have a Document
class representing a document we read from the disk and are editing. Maybe when an instance of this class goes away, we want to make sure that any changes to that document are written back to the disk rather than thrown away.
There's an example a bit like this with the
VideoWriter
class in the ASCIImation assignment (HW 3).You call the
release()
member function to make it write the contents of the video.Until the video file is written, at least some of it is in memory.
It'd be cool if it wrote itself out automatically when the video writer is destroyed.
In fact, it does!
We didn't rely on this functionality, but the destruction phase for
VideoWriter
objects callsrelease()
as part of its cleanup.
Destruction of Primitive Types
When a value of a primitive type (e.g., int
, or double
, or any pointer type) is destroyed, no clean up is necessary.
C++ could do something like zero-out that part of memory, but on most systems, following C++'s No Overhead principle, destruction typically does nothing for values from primitive types.
Destruction of Instances of Classes
Destruction for an instance of a class is much more interesting!
Once again, the author of the class gets to define what should happen!
The function that defines what should happen any time an instance of the class is destroyed is called the destructor.
A destructor's name is the same as the class (and the constructor), but with a ~
in front of it. That character is most properly called a “tilde”, but sometimes coders also call it a “twiddle”.
So the destructor for the Cow
class would be named ~Cow()
.
A Cow
object doesn't have anything particular to do to clean up when it is destroyed, but we'll eventually see examples where the destructor has a really important job to do!
Hayyyy, I remember from math that
~
sometimes means “not”! Is this related?That's a great connection! We can think of the destructor as the “not-
Cow
” function: it unmakes aCow
object!If it's like the opposite of a constructor, can I call it the deconstructor?!?
To avoid confusion, it's best to follow standard terminology. The phase is destruction, and the special member function we implement is the destructor.
And, canine creature, deconstruction is “a method of critical analysis of philosophical and literary language which emphasizes the internal workings of language and conceptual systems, the relational quality of meaning, and the assumptions implicit in forms of expression”, which is clearly a distinct concept.
I have a whole group of friends I hang out with at the local coffee shop, and we were into deconstruction before it was cool!
Synthesized Destructors
Like constructors and assignment operators, the compiler is willing to synthesize a destructor for us.
Destructor Specification
Code | Destructor? | Who Writes It? |
---|---|---|
~Cow(); |
Yes | You |
~Cow() = default; |
Yes | The compiler |
Notes
- For the
default
form, the synthesized destructor will destroy each data member. For primitive data members, that means it will “do nothing” (as above). For data members that are themselves instances of classes, it will call the appropriate destructor. - Notice there's no
delete
form—it's important that you have a destructor, whether you write it yourself or you get the compiler to do it for you. (Keep reading…)
Unlike constructors and assignment operators, it's a (really, really, really) bad idea not to have a destructor at all!
Ah, I see. So we can't just do, like,
~Cow() = delete;
?Oh , you can… After all, this is C++! C++ is sure you know what you are doing when you make it impossible to destroy instances of a class.
As usual, it's up to you to be responsible and not disable the destructor.
Important note! Since the destructor is automatically called whenever an object goes into the destruction phase of its life, you should never, ever, ever (ever) call the destructor directly!
Whoah, it seems super weird to write a function that we never call.
Totally, but that's what you have to do with destructors. You have to trust that the compiler will make a call to the destructor at the right time.
When it's time for the object to be destroyed, the destructor will be called automatically. We promise!
So why can we call it if we're never supposed to?
It's there for experts. In certain advanced contexts it is useful to have this kind of control.
But not in CS 70.
Practice Problem
Suppose our Cow
class defines the following destructor:
Cow::~Cow() {
cout << "Cow destructor called" << endl;
}
And that we write the following program using the Cow
class:
1| void cowGoesMoo(Cow c) {
2| c.moo();
3| }
4|
5| int main() {
6| Cow bessie;
7| cowGoesMoo(bessie);
8| }
(When logged in, completion status appears here.)