CS247 Lecture 10
Last Time: Git, UML, Inheritance This Time: virtual, override, pure virtual, destructors
How do we know which method call in the hierarchy is invoked for b.isHeavry()
or b->Heavy()
?
(Notes copied into Virtual Method)
- Is the method being called on an object? If so, always use the static type to determine which method is called.
- First example: In the first example,
b.isHeavy()
, the objectb
is of typeBook
, and sinceisHeavy()
is a non-virtual method, the method called is determined by the static type ofb
, which isBook
. Therefore, it callsBook::isHeavy()
. - If we call
t.isHeavy()
, exhibits dynamic dispatch. Dynamic dispatch, also known as runtime polymorphism, is the mechanism in C++ by which the appropriate method to be executed is determined at runtime based on the actual type of the object being referred to. This is achieved through the use of virtual functions and the virtual function table (vtable). When you callt.isHeavy()
wheret
is an object of theText
class, the method that gets executed depends on the actual type of the object, which isText
in this case. SinceisHeavy
is declared asvirtual
in the base classBook
and is overridden in the derived classText
, the method call is dynamically dispatched to the version ofisHeavy
defined in theText
class. - In the case of
Book b = Text{...};
, theisHeavy
function call onb
would indeed call theBook::isHeavy()
method. This is because the dynamic type ofb
isBook
even though it was originally assigned aText
object. This scenario demonstrates the principle of using the static type to determine which method is called.Text{...}
creates aText
object. TheText
object is used to initializeb
, which is of typeBook
. This involves a process called object slicing, where only the base class part of the derived object is used to initialize the base class object. Whenb.isHeavy()
is called, it’s based on the static type ofb
, which isBook
. Sinceb
is of typeBook
, theBook::isHeavy()
method is called. Even thoughb
was originally created as aText
object, the static type determines which method is called, and it calls the version ofisHeavy
defined in theBook
class.
When a method is called on an object, the determination of which method to invoke is based on the static type of the object.
- Is the method called via pointer or reference?
a. Is the method NOT declared as
virtual
? Use the static type to determine which method is called:
b. Is the method virtual
? Use the dynamic type to determine which method is called:
We can support:
Each iteration calls a different isHeavy()
method.
What about
(*book).isHeavy()
?
(*book),isHeavy()
calls the correct version/method as well. Why? Because*book
yields aBook&
(i.e. a reference).
What is the purpose of the override
keyword?
- It has no effect on the executable that is created! (WHAT DOES THAT MEAN????????)
- However, it can be helpful for catching bugs.
isHeavy()
is missing a const
. This won’t override Book’s virtual isHeavy
because the signatures do not match.
Specifying override will have the compiler warn you if the signature does not match a superclass’s virtual method.
Compiler will always choose to call a const method to guarantee optimization and correctness.
Why not just declare everything as
virtual
for simplicity?Declaring
doSomething
as virtual doubles the size of our Vec object, program consumes more RAM, slower in general. This extra 8 bytes is storing the vptr - virtual pointer. vptr allows us to achieve dynamic dispatch with virtual functions.
- Declaring
doSomethin()
asvirtual
doubles the size of our Vec object. Program consumes more RAM, slower in general. - This extra 8 bytes is storing the
vptr
- virtual pointer.vptr
allows us to achieve dynamic dispatch withvirtual
functions.
Remember: In MIPS, function calls use the JALR instruction, it saves a register, jumps PC to a specific memory address, hardcoded in the machine instruction.
With dynamic dispatch, which function to jump to could depend on user input. Cannot be hardcoded.
Depending on the dynamic type of the v vptr
it will call Vec2
or Vec3
’s doSomething()
.
When we create a Vec2
or Vec3
, we know what type of object we’re creating, so we can fill in the appropriate vptr
for that object.
vptr
always points to the vtable
which points to the function address.
Now, in either case, we can simply follow the vptr
, get to the vtable
, and find the function address for the doSomething()
method.
Extra running time cost in the time it takes to follow the vptr
and access the vtable
.
C++
philosophy: Don’t pay for costs unless you ask for it.
Destructors Revisited
Which of these leaks memory?
Because the destructor is non-virtual, for pxy
, we invoke ~X
, not the ~Y
, so this array b
is leaked, since the Y
object does not get destroyed.
Solution: declare virtual ~X();
, so delete pxy
will call ~Y()
.
Unless you are sure a class will never be subclassed, then always declare you destructor virtual
.
If you are sure, enforce it via the final
keyword.
Now, the program won’t compile if anyone tries to subclass it.
Object destruction sequence:
- Destructor body runs
- Object fields have their destructors run in reverse declaration order
- Superclass destructor runs
- Spare is reclaimed
Next: CS247 Lecture 11