Content
Basics of Polymorphism Function Overloading (ad hoc polymorphism) Subtype Polymorphism Static and Dynamic Binding dynamic_cast Member Lookup The override keyword Polymorphism and Design Overload vs. Override <- Go BackUniversity of Michigan at Ann Arbor
Last Edit Date: 03/25/2023
Disclaimer and Term of Use:
We do not guarantee the accuracy and completeness of the summary content. Some of the course material may not be included, and some of the content in the summary may not be correct. You should use this file properly and legally. We are not responsible for any results from using this file
This personal note is adapted from Professor Amir Kamil, Andrew DeOrio, James Juett, Sofia Saleem, and Saquib Razak. Please contact us to delete this file if you think your rights have been violated.
This work is licensed under a Creative Commons Attribution 4.0 International License.
The word polymorphism literally means “many forms.” In the context of programming, polymorphism refers to the ability of a piece of code to behave differently depending on the context in which it is used.
There are several forms of polymophism:
ad hoc polymorphism, which refers to function overloading
parametric polymorphism in the form of templates
subtype polymorphism, which allows a derived-class object to be used where a base-class object is expected
The unqualified term “polymorphism” usually refers to subtype polymorphism.
C++ allows both top-level functions and member functions to be overloaded. The following is an example of overloaded member functions:
1 class Base { 2 public: 3 void foo(int a); 4 int foo(string b); 5 }; 6 7 int main() { 8 Base b; 9 b.foo(42); 10 b.foo("test"); 11 }
The call b.foo(42)
calls the member function foo()
with parameter int
, since 42 is an int
.
The call b.foo("test")
calls the function with parameter string
– "test"
actually has type const char *
, but a string parameter is a better match for a const char *
than int
.
In C++, functions can only be overloaded when defined within the same scope. If functions of the same name are defined in a different scope, then those that are defined in a closer scope hide the functions defined in a further scope:
1 class Derived : public Base { 2 public: 3 int foo(int a); 4 double foo(double b); 5 }; 6 7 int main() { 8 Derived d; 9 d.foo("test"); // ERROR 10 }
When handling the member access d.foo
, the compiler finds the name foo
in Derived. It then applies function-overload resolution; however, none of the functions with name foo
can be invoked on a const char *
, resulting in a compile error. The functions inherited from Base
are not considered, since they were defined in a different scope.
Function overloading requires the signatures of the functions to differ, so that overload resolution can choose the overload with the most appropriate signature.
The “signature” refers to the function name and parameter types – the return type is not part of the signature and is not considered in overload resolution.
1 class Person { 2 public: 3 void greet(); 4 void greet(int x); // OK 5 void greet(string x); // OK 6 void greet(int x, string s); // OK 7 void greet(string s, int x); // OK 8 bool greet(); // ERROR: signature the same as the first overload 9 void greet() const; // OK: implicit this parameter different 10 };
Note: For member functions, the const
keyword after the parameter list is part of the signature – it changes the implicit this parameter from being a pointer to non-const to a pointer to const.
Subtype polymorphism allows a derived-class object to be used where a base-class one is expected. In order for this to work, however, we need indirection.
Consider what happens if we directly copy a Chicken
object into a Bird
:
1 int main() { 2 Chicken chicken("Myrtle"); 3 // ... 4 Bird bird = chicken; 5 }
C++ allows this, the value of a Chicken
does not necessarily fit into a Bird
object, since a Chicken
has more member variables than a Bird
. But this might cause object slicing, which copies only the members defined by the base class.
To avoid slicing, we need indirection through a reference or a pointer, so that we avoid making a copy:
Bird &bird_ref = chicken; Bird *bird_ptr = &chicken;
The above initializes bird_ref
as an alias for the chicken
object. Similarly, bird_ptr
is initialized to hold the address of the chicken
object. In either case, a copy is avoided.
C++ allows a reference or pointer of a base type to refer to an object of a derived type. It allows implicit upcasts, which are conversions that go upward in the inheritance hierarchy, such as from Chicken
to Bird
, as in the examples above. On the other hand, implicit downcasts are prohibited because it is unsafe (i.e. some object exists in a chicken may not exist in a bird).
Chicken &chicken_ref = bird_ref; // ERROR: implicit downcast Chicken *chicken_ptr = bird_ptr; // ERROR: implicit downcast
While implicit downcasts are prohibited, we can do explicit downcasts with static_cast
:
Chicken &chicken_ref = static_cast<Chicken &>(bird_ref); Chicken *chicken_ptr = static_cast<Chicken *>(bird_ptr);
These conversions are unchecked at runtime, so we need to be certain from the code that the underlying object is a Chicken.
In order to be able to bind a base-class reference or pointer to a derived-class object, the inheritance relationship must be accessible. From outside the classes, this means that the derived class must publicly inherit from the derived class. Otherwise, the outside world is not allowed to take advantage of the inheritance relationship. Consider this example:
1 class A { 2 }; 3 4 class B : A { // default is private when using the class keyword 5 }; 6 7 int main() { 8 B b; 9 A *a_ptr = &b; // ERROR: inheritance relationship is private 10 }
This results in a compiler error:
main.cpp:9:16: error: cannot cast 'B' to its private base class 'A' A *a_ptr = &b; // ERROR: inheritance relationship is private ^ main.cpp:4:13: note: implicitly declared private here class B : A { // default is private when using the class keyword ^ 1 error generated.
Subtype polymorphism allows us to pass a derived-class object to a function that expects a base-class object:
1 void Image_init(Image* img, istream& is); 2 3 int main() { 4 Image *image = /* ... */; 5 istringstream input(/* ... */); 6 Image_init(image, input); 7 }
Here, we have passed an istringstream
object to a function that expects an istream
. Extracting from the stream will use the functionality that istringstream
defines for extraction.
1 void all_talk(Bird *birds[], int length) { 2 for (int i = 0; i < length; ++i) { 3 array[i]->talk(); 4 } 5 } 6 7 int main() { 8 Chicken c1 = /* ... */; 9 Duck d = /* ... */; 10 Chicken c2 = /* ... */; 11 Bird *array[] = { &c1, &d, &c2 }; 12 all_talk(array, 3); 13 }
Unfortunately, given the way we defined the talk()
member function of Bird
last time, this code will not use the derived-class versions of the function. Instead, all three calls to talk()
will use the Bird
version:
$ ./main.exe tweet tweet tweet
In order to get gynamic binding instead, we need to declare the member function as virtual in the base class:
1 class Bird { 2 ... 3 virtual void talk() const { 4 cout << "tweet" << endl; 5 } 6 };
Now when we call the all_talk()
function, the complier will use the dynamic type of the receiver in the invocation array[i]->talk()
:
$ ./main.exe bawwk quack bawwk
The virtual
keyword is necessary in the base class, but optional in the derived classes. It can only be applied to the declaration within a class; if the function is subsequently defined outside of the class, the definition cannot include the virtual
keyword:
1 class Bird { 2 ... 3 virtual void talk() const; 4 }; 5 6 void Bird::talk() const { 7 cout << "bawwk" << endl; 8 }
In general
The static type of a pointer/reference is the type it is declared with and is known as complie time.
The dynamic type of a pointer/reference is the type of the object it is actually pointing to at run time.
dynamic_cast
¶With dynamic binding, the only change we need to make to our code is to add the virtual
keyword when declaring the base-class member function. No changes are required to the actual function calls (e.g. in all_talk()
).
Consider an alternative to dynamic binding, where we manually check the runtime type of an object to call the appropriate function. In C++, a dynamic_cast
conversion checks the dynamic type of the receiver object:
1 Chicken chicken("Myrtle");
2 Bird *b_ptr = &chicken;
3 Chicken *c_ptr = dynamic_cast<Chicken *>(b_ptr);
4 if (c_ptr) { // check for null
5 // do something chicken-specific
6 }
If the dynamic type is not actually a Chicken
, the conversion results in a null pointer. Otherwise, it results in the address of the Chicken
object.
With indirection, the following is the full lookup process:
The compiler looks up the member in the static type of the receiver object, starting in the class itself, then looking in the base class if necessary. It is an error if no member of the given name is found in the static type or its base types.
If the member found is an overloaded function, then the arguments of the function call are used to determine which overload is called.
If the member is a variable or non-virtual function (including static member functions, which we will see later), the access is statically bound at compile time.
If the member is a virtual function, the access uses dynamic binding. At runtime, the program will look for a function of the same signature, starting at the dynamic type of the receiver, then proceeding to its base type if necessary.
As indicated above, dynamic binding requires two conditions to be met to use the derived-class version of a function:
The member function found at compile time using the static type must be virtual.
The derived-class function must have the same signature as the function found at compile time.
Example:
1 class Top { 2 public: 3 int f1() const { 4 return 1; 5 } 6 7 virtual int f2() const { 8 return 2; 9 } 10 }; 11 12 class Middle : public Top { 13 public: 14 int f1() const { 15 return 3; 16 } 17 18 virtual int f2() const { 19 return 4; 20 } 21 }; 22 23 class Bottom : public Middle { 24 public: 25 int f1() const { 26 return 5; 27 } 28 29 virtual int f2() const { 30 return 6; 31 } 32 }; 33 34 int main() { 35 Top top; 36 Middle mid; 37 Bottom bot; 38 Top *top_ptr = ⊥ 39 Middle *mid_ptr = ∣ 40 41 cout << top.f2() << endl; // prints 2 42 cout << mid.f1() << endl; // prints 3 43 cout << top_ptr->f1() << endl; // prints 1 44 cout << top_ptr->f2() << endl; // prints 6 45 cout << mid_ptr->f2() << endl; // prints 4 46 mid_ptr = ⊥ 47 cout << mid_ptr->f1() << endl; // prints 3 48 cout << mid_ptr->f2() << endl; // prints 6 49 }
Try it out:
override
keyword¶Only virtual funcitons can be overridden, at least base class needs it. Note that only functions with same signature can override.
We may not need virtual
keyword in a derived class, but override
keyword is needed, which means in a drived class the following are the same:
virtual int function() const {...}
int function() const override {...}
When a base class defines a function as virtual, it means the derived classes may provide an overridden version if they want.
When a base class defines a function as pure virtual, it is an abstract class and cannot be instantiated. Derived classes must provide an overridden version (or else remain themselves abstract).
A pure virtual function looks like the following:
1 class Bird {
2 ...
3 virtual int get_wingspan() const = 0;
4 };
Note: Always use override
keyword in derived class in these cases above.
Function overloading (achieved at compile time) provides multiple definitions of the function by changing signature. (Base and derived classes).
Function overriding (achieved at run time) redefines the base class function in its derived class with the same signature. (Derived class only, virtual
or override
keywords are needed)