CSE332S Object-Oriented Programming in C++ (Lecture 12)
Object-Oriented Programming (OOP) in C++
Today:
- Type vs. Class
- Subtypes and Substitution
- Polymorphism a. Parametric polymorphism (generic programming) b. Subtyping polymorphism (OOP)
- Inheritance and Polymorphism in C++ a. construction/destruction order b. Static vs. dynamic type c. Dynamic binding via virtual functions d. Declaring interfaces via pure virtual functions
Type vs. Class, substitution
Type (interface) vs. Class
Each function/operator declared by an object has a signature: name, parameter list, and return value
The set of all public signatures defined by an object makes up the interface to the object, or its type
- An object’s type is known (what can we request of an object?)
- Its implementation is not - different objects may implement an interface very differently
- An object may have many types (think interfaces in Java)
An object’s class defines its implementation:
- Specifies its state (internal data and its representation)
- Implements the functions/operators it declares
Subtyping: Liskov Substitution Principle
An interface may contain other interfaces!
A type is a subtype if it contains the full interface of another type (its supertype) as a subset of its own interface. (subtype has more methods than supertype)
Substitutability: if S is a subtype of T, then objects of type T may be replaced with objects of type S
Substitutability leads to polymorphism: a single interface may have many different implementations
Polymorphism
Parametric (interface) polymorphism (substitution applied to generic programming)
- Design algorithms or classes using parameterized types rather than specific concrete data types.
- Any class that defines the full interface required of the parameterized type (is a subtype of the parameterized type) can be substituted in place of the type parameter at compile-time.
- Allows substitution of unrelated types.
Polymorphism in OOP
Subtyping (inheritance) polymorphism: (substitution applied to OOP)
- A derived class can inherit an interface from its parent (base) class
- Creates a subtype/supertype relationship. (subclass/superclass)
- All subclasses of a superclass inherit the superclass’s interface and its implementation of that interface.
- Function overriding - subclasses may override the superclass’s implementation of an interface
- Allows the implementation of an interface to be substituted at run-time via dynamic binding
Inheritance in C++ - syntax
Forms of Inheritance in C++
A derived class can inherit from a base class in one of 3 ways:
- Public Inheritance (“is a”, creates a subtype)
- Public part of base class remains public
- Protected part of base class remains protected
- Protected Inheritance (“contains a”, derived class is not a subtype)
- Public part of base class becomes protected
- Protected part of base class remains protected
- Private Inheritance (“contains a”, derived class is not a subtype)
- Public part of base class becomes private
- Protected part of base class becomes private
So public inheritance is the only way to create a subtype.
class A {
public:
int i;
protected:
int j;
private:
int k;
};
class B : public A {
// ...
};
class C : protected A {
// ...
};
class D : private A {
// ...
};Class B uses public inheritance from A
iremains public to all users of class Bjremains protected. It can be used by methods in class B or its derived classes
Class C uses protected inheritance from A
ibecomes protected in C, so the only users of class C that can accessiare the methods of class Cjremains protected. It can be used by methods in class C or its derived classes
Class D uses private inheritance from A
iandjbecome private in D, so only methods of class D can access them.
Construction and Destruction Order of derived class objects
Class and Member Construction Order
class A {
public:
A(int i) : m_i(i) {
cout << "A" << endl;}
~A() {cout<<"~A"<<endl;}
private:
int m_i;
};
class B : public A {
public:
B(int i, int j)
: A(i), m_j(j) {
cout << "B" << endl;}
~B() {cout << "~B" << endl;}
private:
int m_j;
};
int main (int, char *[]) {
B b(2,3);
return 0;
};In the main function, the B constructor is called on object b
- Passes in integer values 2 and 3
B constructor calls A constructor
- passes value 2 to A constructor via base/member initialization list
A constructor initializes m_i with the passed value 2
- Body of A constructor runs
- Outputs “A”
B constructor initializes m_j with passed value 3
- Body of B constructor runs
- outputs “B”
Class and Member Destruction Order
class A {
public:
A(int i) : m_i(i) {
cout << "A" << endl;}
~A() {cout<<"~A"<<endl;}
private:
int m_i;
};
class B : public A {
public:
B(int i, int j)
: A(i), m_j(j) {
cout << "B" << endl;}
~B() {cout << "~B" << endl;}
private:
int m_j;
};
int main (int, char *[]) {
B b(2,3);
return 0;
}; B destructor called on object b in main
- Body of B destructor runs
- outputs “~B”
B destructor calls “destructor” of m_j
- int is a built-in type, so it’s a no-op
B destructor calls A destructor
- Body of A destructor runs
- outputs “~A”
A destructor calls “destructor” of m_i
- again a no-op
At the level of each class, order of steps is reversed in constructor vs. destructor
- ctor: base class, members, body
- dtor: body, members, base class
In short, cascading order is called when constructor is called, and reverse cascading order is called when destructor is called.
Polymorphic function calls - function overriding
Static vs. Dynamic type
The type of a variable is known statically (at compile time), based on its declaration
int i; int * p;
Fish f; Mammal m;
Fish * fp = &f;However, actual types of objects aliased by references & pointers to base classes vary dynamically (at run-time)
Fish f; Mammal m;
Animal * ap = &f; // dynamic type is Fish
ap = &m; // dynamic type is Mammal
Animal & ar = get_animal(); // dynamic type is the type of the object returned by get_animal()A base class and its derived classes form a set of types
type(*ap) {Animal, Fish, Mammal}
typeset(*fp) typeset(*ap)
Each type set is open
- More subclasses can be added
Supporting Function Overriding in C++: Virtual Functions
Static binding: A function/operator call is bound to an implementation at compile-time
Dynamic binding: A function/operator call is bound to an implementation at run-time. When dynamic binding is used:
- Lookup the dynamic type of the object the function/operator is called on
- Bind the call to the implementation defined in that class
Function overriding requires dynamic binding!
In C++, virtual functions facilitate dynamic binding.
class A {
public:
A () {cout<<" A";}
virtual ~A () {cout<<" ~A";} // tells compiler that this destructor might be overridden in a derived class (the destructor of the parent class is usually virtual)
virtual void f(int); // tells compiler that this function might be overridden in a derived class
};
class B : public A {
public:
B () :A() {cout<<" B";}
virtual ~B() {cout<<" ~B";}
virtual void f(int) override; // tells compiler that this function might be overridden in a derived class, the parent function is virtual otherwise it will be an error
//C++11
};
int main (int, char *[]) {
// prints "A B"
A *ap = new B;
// prints "~B ~A" : would only
// print "~A" if non-virtual
delete ap;
return 0;
};Virtual functions:
-
Declared virtual in a base class
-
Can override in derived classes
-
Overriding only happens when signatures are the same
-
Otherwise it just overloads the function or operator name
When called through a pointer or reference to a base class:
- function/operator calls are resolved dynamically
Use final (C++11) to prevent overriding of a virtual method
Use override (C++11) in derived class to ensure that the signatures match (error if not)
class A {
public:
void x() {cout<<"A::x";};
virtual void y() {cout<<"A::y";};
};
class B : public A {
public:
void x() {cout<<"B::x";};
virtual void y() {cout<<"B::y";};
};
int main () {
B b;
A *ap = &b; B *bp = &b;
b.x (); // prints "B::x": static binding always calls the x() function of the class of the object
b.y (); // prints "B::y": static binding always calls the y() function of the class of the object
bp->x (); // prints "B::x": lookup the type of bp, which is B, and x() is non-virtual so it is statically bound
bp->y (); // prints "B::y": lookup the dynamic type of bp, which is B (at run-time), and call the overridden y() function
ap->x (); // prints "A::x": lookup the type of ap, which is A, and x() is non-virtual so it is statically bound
ap->y (); // prints "B::y": lookup the dynamic type of ap, which is B (at run-time), and call the overridden y() function of class B
return 0;
};Only matter with pointer or reference
- Calls on object itself resolved statically
- E.g.,
b.y();
Look first at pointer/reference type
- If non-virtual there, resolve statically
- E.g.,
ap->x(); - If virtual there, resolve dynamically
- E.g.,
ap->y();
Note that virtual keyword need not be repeated in derived classes
- But it’s good style to do so
Caller can force static resolution of a virtual function via scope operator
- E.g.,
ap->A::y();prints “A::y”
Potential Problem: Class Slicing
When a derived type may be caught by a catch block, passed into a function, or returned out of a function that expects a base type:
- Be sure to catch by reference
- Pass by reference
- Return by reference
Otherwise, a copy is made:
- Loses original object’s “dynamic type”
- Only the base parts of the object are copied, resulting in the class slicing problem
Class (implementation) Inheritance VS. Interface
Inheritance Class is the implementation of a type.
- Class inheritance involves inheriting interface and implementation
- Internal state and representation of an object
Interface is the set of operations that can be called on an object.
- Interface inheritance involves inheriting only a common interface
- What operations can be called on an object of the type?
- Subclasses are related by a common interface
- But may have very different implementations
In C++, pure virtual functions make interface inheritance possible.
class A { // the abstract base class
public:
virtual void x() = 0; // pure virtual function, no default implementation
virtual void y() = 0; // pure virtual function, no default implementation
};
class B : public A { // B is still an abstract class because it still has a pure virtual function y() that is not defined
public:
virtual void x();
};
class C : public B { // C is a concrete derived class because it has all the pure virtual functions defined
public:
virtual void y();
};
int main () {
A * ap = new C; // ap is a pointer to an abstract class type, but it can point to a concrete derived class object, cannot create an object of an abstract class, for example, new A() will be an error.
ap->x ();
ap->y ();
delete ap;
return 0;
};Pure Virtual Functions and Abstract Base Classes:
A is an abstract (base) class
- Similar to an interface in Java
- Declares pure virtual functions (=0)
- May also have non-virtual methods, as well as virtual methods that are not pure virtual
Derived classes override pure virtual methods
- B overrides
x(), C overridesy()
Can’t instantiate an abstract class
- class that declares pure virtual functions
- or inherits ones that are not overridden
A and B are abstract, can create a C
Can still have a pointer or reference to an abstract class type
- Useful for polymorphism
Review of Inheritance and Subtyping Polymorphism in C++
Create related subclasses via public inheritance from a common superclass
- All subclasses inherit the interface and its implementation from the superclass
Override superclass implementation via function overriding
- Relies on virtual functions to support dynamic binding of function/operator calls
Use pure virtual functions to declare a common interface that related subclasses can implement
- Client code uses the common interface, does not care how the interface is defined. Reduces complexity and dependencies between objects in a system.