Polymorphism and Virtual Functions
|
Learning
Objectives After completing this module,
students should be able to: •
Define polymorphism and distinguish compile-time from run-time
polymorphism. •
Use base-class pointers and references to derived objects and
explain why a non-virtual call uses the pointer type. •
Declare virtual functions, override them correctly with override, and explain
the virtual attribute in inheritance. •
Explain overriding versus hiding and overloading, covariant
return types, default arguments and object slicing. •
Describe how virtual functions are implemented with a vptr and a
vtable. |
1. Introduction to Polymorphism
|
Definition: Polymorphism
(Greek: “many forms”) is the ability of one name, interface or call to have different
behaviour depending on the type of object involved. |
|
Kind |
When
the function is chosen |
Mechanism |
Example |
|
Compile-time (static) polymorphism |
During compilation |
Function overloading, operator overloading, templates |
add(int,
int) and add(double, double) |
|
Run-time (dynamic) polymorphism |
While the program runs, from the real type of the object |
Virtual functions and inheritance |
animal->sound() runs Dog::sound() or Cat::sound() |
Program 5.1: Compile-time polymorphism by
overloading.
#include <iostream>
using namespace std;
class Calculator {
public:
int add(int
a, int b) { return a + b; }
double
add(double a, double b) { return a + b; }
};
int main()
{
Calculator
c;
cout
<< c.add(10, 20) << '\n';
// add(int, int)
cout
<< c.add(2.5, 3.5) << '\n';
// add(double, double)
return 0;
}
Output:
30
6
The compiler selects add(int, int)
or add(double, double) from the
argument types, before the program runs. The rest of this unit is about run-time
polymorphism, which needs inheritance and virtual functions.
2. Base-Class Pointers and
References to Derived Objects
A pointer or reference to a base class may refer to an object of any publicly
derived class (Module 1). This is what makes it possible to write code that
works with a whole family of classes through one type.
Dog dog;
Animal *ptr = &dog; // base pointer, derived object
Animal &ref = dog; // base reference, derived object
Two types are involved:
•
the static type – the declared type of the
pointer or reference (Animal),
known at compile time;
•
the dynamic type – the real type of the object
it refers to (Dog), known only at run time.
Which function runs when the pointer is used for a call depends on
whether the function is virtual.

Figure 5.1: One
call through a base pointer, many possible functions.
Program 5.2: A virtual and a non-virtual
function called through a base pointer.
#include <iostream>
using namespace std;
class Base {
public:
virtual
void show() { cout << "Base show()" << '\n'; }
void
display() { cout << "Base display()" << '\n'; }
virtual
~Base() = default;
};
class Derived : public Base {
public:
void show()
override { cout << "Derived show()" << '\n'; }
void
display() { cout << "Derived display()" << '\n'; }
};
int main()
{
Derived d;
Base *ptr =
&d;
ptr->show();
// virtual: Derived::show()
ptr->display();
// non-virtual: Base::display()
return 0;
}
Output:
Derived show()
Base display()
•
show()
is virtual: the call through the Base * uses the dynamic type and runs Derived::show().
•
display()
is not virtual: the call uses the static type Base
and runs Base::display(), although the
object is a Derived.
|
Important: Without virtual,
a function declared in the derived class only hides the base function;
calls through a base pointer still use the base version. virtual
is what turns hiding into overriding. |
3. Virtual Functions
|
Definition: A
virtual function is a member function declared with the keyword virtual
in a base class, which a derived class can override. When it is called
through a base-class pointer or reference, the version belonging to the actual
object is executed. |
class Base {
public:
virtual
void show(); // may be
overridden
virtual
~Base(); // virtual
destructor
};
class Derived : public Base {
public:
void show()
override; // overrides
Base::show
};
Program 5.3: Animals that make different
sounds.
#include <iostream>
using namespace std;
class Animal {
public:
virtual
void sound() { cout << "Animal makes a sound" << '\n'; }
virtual
~Animal() = default;
};
class Dog : public Animal {
public:
void
sound() override { cout << "Dog barks" << '\n'; }
};
class Cat : public Animal {
public:
void
sound() override { cout << "Cat meows" << '\n'; }
};
int main()
{
Dog dog;
Cat cat;
Animal
*ptr;
ptr =
&dog;
ptr->sound();
ptr =
&cat;
ptr->sound();
return 0;
}
Output:
Dog barks
Cat meows
1.
sound()
is virtual in Animal and overridden in Dog and Cat.
2.
The same statement ptr->sound();
runs Dog::sound() when ptr points to a Dog
and Cat::sound() when it points to a Cat.
3.
A new animal class can be added later without changing
the code that calls sound() – the main
benefit of polymorphism.
4. The Virtual Attribute Is
Inherited
Once a function is virtual in a base class, it is virtual in every
derived class, whether or not the keyword virtual
is repeated. Each class in a chain may override it, and a class that does not
override it simply inherits the nearest version.
Program 5.4: A three-level chain; one class
does not override.
#include <iostream>
using namespace std;
class Base {
public:
virtual
void display() { cout << "Base class" << '\n'; }
virtual
~Base() = default;
};
class Intermediate : public Base {
public:
void
display() override { cout << "Intermediate class" <<
'\n'; }
};
class Derived : public Intermediate {
public:
void
display() override { cout << "Derived class" << '\n'; }
};
class Plain : public Intermediate {}; // does not override display()
int main()
{
Derived
obj;
Base
&ref = obj;
ref.display();
// final override: Derived::display()
Plain p;
Base
&ref2 = p;
// nearest
override: Intermediate::display()
ref2.display();
return 0;
}
Output:
Derived class
Intermediate class
•
For the Derived
object the most derived override runs.
•
Plain
does not override display(), so a
call through a Base & runs the nearest
version above it, Intermediate::display().
•
Writing virtual
again in the derived class is allowed but unnecessary; the keyword override documents the intention and lets the compiler
check it (Section 5).
Hierarchies of several levels, and the case in which a function is not
defined at all in the base class (pure virtual functions), are discussed in
Module 6.
5. Overriding, Overloading and
Hiding
|
Term |
Meaning |
Resolved |
|
Overloading |
Same name, different parameters, in the same scope |
At compile time |
|
Overriding |
A derived class redefines a virtual function with
the same signature |
At run time |
|
Hiding |
A derived function with the same name (any signature,
virtual or not) hides base functions of that name |
At compile time |
5.1 The signature must match
A derived function overrides a virtual function only if its name,
parameter types and const-qualification
are the same. If the signature differs even slightly, the derived function is a
new, unrelated function and nothing is overridden.
Program 5.5: A missing const silently breaks overriding.
#include <iostream>
using namespace std;
class Base {
public:
virtual
void show() const { cout << "Base::show() const" << '\n';
}
virtual
~Base() = default;
};
class Derived : public Base {
public:
// not
const: a NEW function
void show()
{ cout << "Derived::show()" << '\n'; }
};
int main()
{
Derived d;
Base *p =
&d;
p->show(); //
calls Base::show() const: nothing was overridden
d.show(); //
calls Derived::show()
return 0;
}
Output:
Base::show() const
Derived::show()
Base::show() const and Derived::show() have different signatures, so the call
through the base pointer runs the base version. Some compilers warn about it:
warning: ‘virtual void Base::show() const’ was hidden
[-Woverloaded-virtual=]
5.2 The keyword override
Writing override after the declaration
tells the compiler that the function is meant to override a virtual function.
If it does not, the compiler reports an error instead of silently creating a
new function:
error: ‘void Derived::show()’ marked ‘override’, but
does not override
|
Version Note: override
(and final) are C++11 features.
In C++98/03 they do not exist: write the function with the same signature and
take care by hand. GCC in C++98 mode accepts the keyword only as an
extension, with a warning: |
warning: override controls (override/final) only
available with ‘-std=c++11’ or ‘-std=gnu++11’ [-Wc++11-extensions]
Use override on every overriding
function in modern C++.
6. Rules for Virtual Functions
|
Rule |
Explanation |
|
Only member functions |
Non-member functions and friend functions cannot be virtual |
|
Not static |
A static function has no object, so it cannot be virtual |
|
Not constructors |
A constructor builds the object, so its type is not yet
known; it cannot be virtual |
|
Destructors can (and should) be |
Make the destructor virtual in a base class that is used
polymorphically |
|
Same signature |
Name, parameters and const must match to override |
|
Return type |
Must be the same, or a covariant pointer/reference
type (Section 7) |
|
Access specifier |
Is not part of the override rule: a private virtual
function can be overridden, and the access checked is that of the static type |
|
Inheritance |
Virtual functions are inherited; a derived class need not
override them |
|
Declaration in the base |
A function must be declared virtual
in some base class to be called polymorphically |
|
Note: Calling through an object
(not a pointer or reference) is resolved at compile time, because the type of
the object is known: d.show() uses Derived::show()
directly. |
7. Covariant Return Types
An overriding function may return a pointer or reference to a more
derived class than the base function returns. The classic use is a virtual
copy function, clone().
Program 5.6: Derived::clone()
returns Derived *.
#include <iostream>
using namespace std;
class Base {
public:
virtual
Base *clone() const { cout << "Base::clone"
<< '\n'; return new Base(*this); }
virtual
const char *name() const { return "Base"; }
virtual
~Base() = default;
};
class Derived : public Base {
public:
/*
covariant return type: Derived* instead of Base* */
Derived
*clone() const override
{
cout
<< "Derived::clone" << '\n';
return
new Derived(*this);
}
const char
*name() const override { return "Derived"; }
};
int main()
{
Derived d;
Base *b =
&d;
Base *copy1
= b->clone(); //
dynamic: Derived::clone
Derived
*copy2 = d.clone(); // no
cast needed: returns Derived*
cout
<< copy1->name() << " " << copy2->name()
<< '\n';
delete
copy1;
delete
copy2;
return 0;
}
Output:
Derived::clone
Derived::clone
Derived Derived
Through the base pointer, clone()
still runs Derived::clone() and returns a Base * to the new object. Called on a Derived directly, it returns a Derived
* with no cast. This relaxation of the return type is allowed only for
pointers or references to classes.
8. Virtual Functions and Default
Arguments
Default argument values are not overridden: they are bound at
compile time from the static type of the expression, while the function body is
selected at run time.
Program 5.7: Different defaults in base and
derived classes.
#include <iostream>
using namespace std;
class Base {
public:
virtual
void print(int x = 10) { cout << "Base::print " << x
<< '\n'; }
virtual
~Base() = default;
};
class Derived : public Base {
public:
void
print(int x = 20) override { cout << "Derived::print " <<
x << '\n'; }
};
int main()
{
Derived d;
Base *p =
&d;
// body of
Derived::print, default value of Base::print
p->print();
d.print(); // body
and default value of Derived::print
return 0;
}
Output:
Derived::print 10
Derived::print 20
Through the Base * the body of Derived::print runs, but with the default value 10 from Base.
Do not give different default arguments to overriding functions.
9. Object Slicing
Polymorphism works only through pointers and references. If a
derived object is copied into a base object or passed to a function by
value, only the base part is copied (sliced) and the new object is a
genuine Base; its virtual calls cannot
reach the derived functions.
Program 5.8: Slicing by assignment and by
passing by value.
#include <iostream>
using namespace std;
class Base {
public:
virtual
void show() const { cout << "Base::show()" << '\n'; }
virtual
~Base() = default;
};
class Derived : public Base {
public:
void show()
const override { cout << "Derived::show()" << '\n'; }
};
// the argument is a copy of Base part
void byValue(Base b) { b.show(); }
void byReference(const Base &b) { b.show(); }
int main()
{
Derived d;
Base sliced
= d; // slicing: only the Base
part is copied
sliced.show();
byValue(d);
byReference(d);
return 0;
}
Output:
Base::show()
Base::show()
Derived::show()
The first two lines show slicing; the reference version shows the correct
behaviour. In polymorphic code pass objects by reference or pointer (or
smart pointer), and store pointers, not objects, in containers.
10. Preventing Overriding: final
A virtual function declared final
cannot be overridden any more, and a class declared final
cannot be used as a base class.
Program 5.9: A final function and a final
class.
#include <iostream>
using namespace std;
class Base {
public:
virtual
void run() { cout << "Base::run" << '\n'; }
virtual
void stop() { cout << "Base::stop" << '\n'; }
virtual
~Base() = default;
};
class Middle : public Base {
public:
void run()
override final { cout << "Middle::run (final)" << '\n'; }
void stop()
override { cout << "Middle::stop" << '\n'; }
};
class Last final : public Middle { // no class may derive from Last
public:
void stop()
override { cout << "Last::stop" << '\n'; }
// void
run() override {} //
error: run() is final in Middle
};
int main()
{
Last x;
Base &b
= x;
b.run();
b.stop();
return 0;
}
Output:
Middle::run (final)
Last::stop
Attempting to override a final function or to derive from a final class
gives errors:
error: virtual function ‘virtual void Last::run()’
overriding final function
error: cannot derive from ‘final’ base ‘A’ in derived
type ‘B’
|
Version Note: final
and override
are contextual keywords introduced in C++11; they are not available in
C++98/03. |
11. How Virtual Functions Are
Implemented
A typical compiler implements run-time polymorphism with two structures.
•
A vtable (virtual table): for each class that
has virtual functions, one table of function pointers, one slot per virtual
function (an overriding class stores its own function, an inheriting class the
inherited one).
•
A hidden vptr (virtual pointer) stored in every
object of such a class, which points to the vtable of the object’s real
class.
Figure 5.2: An
object holds a vptr to the vtable of its class; a virtual call reads the
function from the table.
A virtual call ptr->show() is
compiled as: read the vptr of the object, take the entry of the show slot from the vtable, and call it. A non-virtual
call is a direct call fixed at compile time (Module 7).
Program 5.10: The hidden pointer enlarges
the object.
#include <iostream>
using namespace std;
class Plain {
int x;
public:
void f() {}
};
class Poly {
int x;
public:
virtual
void f() {}
};
int main()
{
cout
<< "sizeof(Plain) = " << sizeof(Plain) << '\n';
cout
<< "sizeof(Poly) = "
<< sizeof(Poly) << "
(extra hidden vptr)\n";
return 0;
}
Output (GCC, 64-bit; the exact sizes depend
on the compiler and padding):
sizeof(Plain) = 4
sizeof(Poly) =
16 (extra hidden vptr)
The class with a virtual function is larger by the hidden vptr (plus
alignment padding). The vtable itself exists only once per class, not per
object. The cost of a virtual call is one extra memory access and, usually, the
loss of inlining.
Figure 5.3: The
compiler decides between early and late binding according to whether the
function is virtual.
|
Note: The implementation
with a vptr and vtable is the usual one, but the C++ standard only specifies
the behaviour, not how it is achieved. |
12. Advantages and Limitations
|
Advantages |
Limitations |
|
One interface, many implementations; code works with new
classes unchanged |
Small run-time overhead (indirect call, larger objects) |
|
Extensible design: add a derived class without editing the
callers |
Needs pointers or references (slicing otherwise) |
|
Cleaner code: no long if/switch
on the type of an object |
Wrong signature silently fails (use override) |
|
Basis of frameworks, plug-ins and interfaces |
Hierarchies can be hard to follow in large programs |
13. Quick Revision
•
Run-time polymorphism = virtual function called through
a base pointer or reference; the object’s dynamic type decides.
•
Non-virtual member functions use the static type; a
derived function with the same name only hides the base one.
•
Signature (including const)
must match to override; use override
(C++11). final stops further overriding.
•
Covariant returns are allowed for pointers/references
to classes; default arguments are not overridden.
•
Copying a derived object into a base object slices it.
•
Implementation: vptr in the object, vtable per class.
14. Important Examination
Questions
Short-answer questions
1.
Define polymorphism. Name its two types.
2.
What is a virtual function? Write its syntax.
3.
Why is a base-class pointer to a derived object useful?
4.
What happens if a function is not declared virtual and
is called through a base pointer?
5.
Differentiate between overloading, overriding and
hiding.
6.
What is the purpose of the keyword override? In which C++ version was it introduced?
7.
What is a covariant return type?
8.
What is object slicing?
9.
Explain the terms vptr and vtable.
Long-answer and
programming questions
1.
Explain run-time polymorphism with a program using a
base class Animal and derived classes Dog and Cat.
2.
Explain the rules for virtual functions.
3.
Write a program that shows the difference between a
virtual and a non-virtual function called through a base pointer.
4.
Explain how virtual functions are implemented using a
vtable.
5.
Write a program that demonstrates that a missing const prevents overriding.
No comments:
Post a Comment