STAT 598W: Lecture 12 Purdue University More on Inheritance and Related Topics
Jan 14, 2016
STAT 598W: Lecture 12
Purdue UniversityMore on Inheritance and
Related Topics
Topics
Inheritance Polymorphism Virtual methods Abstract classes
Inheritance
Inheritance is the property that instances of a child class can access data and behavior of the parent class(s). Parent superclass; child subclass A CEO is a manager is an employee A dog is a mammal is an animal is a
living thing...
Benefits of Inheritance Software reusability: inherited behavior
doesn't have to be rewritten, and thus will save time and likely be more reliable.
Code sharing: separate users use the same classes; two subclasses use facilities of a common superclass.
Consistent interface: by inheriting methods, the interface to subclasses will be largely consistent. Objects that are almost the same will have interfaces which are almost the same.
Benefits of Inheritance Software components: “off-the-shelf”
software units, e.g., Microsoft's Microsoft Foundation Classes library, QuantLib, the Interactive Brokers C++ API.
Rapid prototyping: concentrate only on portions of a new system which are different than previous ones. Get something running sooner, for early evaluation.
Benefits of Inheritance Polymorphism:
Software is typically designed top-down, written bottom-up. Inheritance encourages abstract superclasses, specialized for particular circumstances.
So rather than having only very low-level code reusable, code at the highest level of abstraction can be reused.
Polymorphic objects can react differently depending on the type of input they are given.
Costs of Inheritance Execution speed: lots of function calls, very
general methods for dealing with arbitrary subclasses. We are getting better at this…
Program size: a library of objects may have just what you want, but it may be “spread out.” Purpose-built code will likely be smaller.
Other types of complexity: flow of control in OOP programs can be just as hard to trace.
Implementation Details
Consider a class for employees:class Employee {public: string name; int age; int department; int salary; Employee * next; // link to employee list // ... };
This example comes from Stroustrup
Managers Are Objects Too
So far, so good, but…
class Manager { Employee emp; // manager's employee record Employee* group; // people managed int level; // high, higher, highest // ... };
A Problem, and a Solution A manager is an employee, so Employee data
is stored in the emp member of Manager. But how to get a Manager into the linked list of employees?
A Manager* is different from an Employee*. A better way: a Manager is an Employee, so
make it a subclass: class Manager : public Employee { // public inheritance Employee* group; short level; // ... };
Subclass Advantages
Now we can create a list of employees, some of whom are managers: Employee * makeList() {
Manager m1, m2; Employee e1, e2; Employee * elist; elist = &m1; // put m1 on elist m1.next = &e1; // put e1 on elist e1.next = &m2; // put m2 on elist m2.next = &e2; // put e2 on elist e2.next = 0; // terminate elist return elist;}
How Does This Work?
This works because a manager is an employee, so an Employee * can point to a Manager.
This doesn't work the other way around, unless there is explicit pointer type conversion. (Which is really really dangerous!)
Let's Add Some Methods
class Employee { string name; // ... public: Employee* next; void print(); // ... }; class Manager : public Employee { // ... public: void print(); // ... };
This Would Seem Natural
But derived classes can't access private portions of the base class. Why? If they could, then private stuff wouldn't
really be private. Anyone could construct a derived class
and have access.
void Manager::print() { cout << "name is " << name << '\n'; // error }
The Good Solution Derived classes cannot access private
members of the base class. But they can access public members:
Note the use of the scope resolution operator, needed since print() is being redefined in Manager. (What happens if we forget to say Employee::?)
void Manager::print() { Employee::print(); // print employee info, then // print manager info }
The Not-So-Good Solution
Make name a protected member of the Employee class, so subclasses (like Manager) have access
This is generally a bad idea, but everyone does it.
Why bad? It breaks encapsulation; anyone can write a subclass to get access.
The Inevitable Student Class
#include <iostream>#include <string>using namespace std;
enum studentYear {freshman, sophomore, junior, senior, graduate};
class Student {protected: int studentID; double gpa; studentYear y; string name;public: Student(int id, double g, studentYear x, string nm); void print() const ;};
GradStudents are Students
enum support { ta, ra, fellow, other};
class GradStudent: public Student {protected: support s; string dept; string thesis;public: GradStudent(int id, double g, year x, string nm, support t, string d, string th); void print() const;};
Student members have to be protected for this to work
New Concepts Which base class members are accessible to
derived classes? The choices are public, protected, private.
public means that protected and public members of Student are available to GradStudent. This is the normal (but not default) case.
Note that private members of the base class are never available to subclasses. Why?
It is typical for a subclass to add new members, both data and methods. Note print() is an overridden method, a concept different from overloading. The signature is the same, but the owner is different.
Code Reuse With Inheritance
Student::Student(int id, double g, studentYear x, string nm) :studentID(id), gpa(g), y(x), name(nm) {} GradStudent::GradStudent(int id, double g, studentYear x, string nm, support t, string d, string th) :Student(id, g, x, nm), s(t), dept(d), thesis(th) {}
It’s common for the constructor of the derived class to call the base class constructor. Now e.g., name can be private.
The print() Methods
void Student::print() const { cout << name << ", " << studentID << ", " << (int)y << ", " << gpa;}
void GradStudent::print() const { Student::print(); cout << ", " << dept << ", " << (int)s << ", " << thesis;}
Finally, a Driver Program
void main() { Student s(365, 2.53, sophomore, "Larry Fine"); Student* ps = &s; GradStudent gs(366, 2.03, graduate, "Curly Howard", ta, "Theatre", "Nyuk Nyuk Nyuk"); GradStudent* pgs; ps->print(); cout << endl; ps = pgs = &gs; // implicit conversion of gradStudent* to student* ps->print(); // student::print() cout << endl; pgs->print(); // gradStudent::print() cout << endl;}
Here is the Output
Larry Fine, 365, 1, 2.53Curly Howard, 366, 4, 2.03Curly Howard, 366, 4, 2.03, Theatre, 0, Nyuk Nyuk Nyuk
Static and Dynamic Typing Binding time: the time when an attribute or
meaning of a a program construct is determined. For instance, in strongly typed languages,
variable names are bound to variable types at compile time (e.g., int a). This leaves no room for variables to take on other types (c.f. variants in VBA).
Dynamically typed languages need some sort of run time system, for example to determine variable types and bind appropriate operators.
Essentially, the question is, are types associated with variables (names) or with values?
Static and Dynamic Typing If x and y are declared ints, then the
compiler knows what to do with the expression x + y.
In OOP, there are additional problems. We saw before that pointers to base class types are valid pointers to subclass objects. This violates the spirit of “strong typing”.
Similarly, an object of a subclass type is a member of the superclass, so it is possible to make an assignment.
Employees and Managers
void main() { Employee e; Manager m; e.setSalary(30000); e.print(); m.setSalary(40000); m.setLevel(4); m.print(); //m = e; error: an e isn't an m e = m; // Legal: an m is an e e.print(); // But, Employee::print() is called}
The “Container Problem” If we view m as an Employee, can we
ever recover that m is really a Manager?
We would like to write abstract “container classes” like sets and lists, but in C++, heterogeneous classes are harder than homogeneous ones.
But if we use pointers to elements of collections, things can be done.
Solving the Problem Given a pointer of type base *, how do
we know if it points to an object of type base or to some derived type?
Three possible solutions: Ensure that only objects of a single type are
pointed to. This insists on homogeneous containers.
Place a “type field” in the base class for functions to inspect.
Use “virtual functions”.
Example of a Type Field
struct employee { // note this is a struct; everything public enum emp_type { M, E}; emp_type type; employee * next; char * name; // ...}
struct manager : employee { // also a struct employee * group; short level; // ... }
Then, Define
void employee_print(employee * e) { switch (e -> type) { case E: cout << e->name << '\t' << e->department << '\n'; break; case M: cout << e->name << '\t' << e->department << '\n'; manager * p = (manager*)e; cout << "level " << p->level << '\n'; break; }
This is a really bad idea. Don’t do it!
Printing the Employee List
A function to print employees might go like:
But what happens if a new subclass is defined? Go through all the code?
void f(employee * elist) { for (; elist; elist = elist->next) print_employee(elist); }
Static and Dynamic Binding If a message is passed to a receiver,
which method responds to the message? On one hand, this is obvious: if the
receiver knows its type, then it will perform the method associated with that type, or look upwards.
On the other hand, there has to be a mechanism to “find an object's type,” and this may be expensive at run time.
Static and Dynamic Binding Static binding if the declared object type
determines the method to use. Dynamic binding if the actual type (at
run time) determines the method to use. C++ “prefers” to use static binding,
since it imposes no additional overhead. C++ can be forced to use dynamic
binding if necessary (the programmer has to ask for it explicitly).
Virtual Functions
For dynamic binding, C++ gives us virtual functions.
The virtual keyword says that a function may be overridden in a subclass, and that the type of the object receiving a message should determine which method (function) to use.
Virtual Function Example
class Base {public: int i; virtual void print_i() { cout << i << " inside Base\n"; } };
class Derived : public Base { public: virtual void print_i() { cout << i << " inside Derived\n"; } };
This is new
Virtual Function Example
void main() { Base b; Base* pb = &b; Derived d; b.i = 1 d.i = 2; pb->print_i(); pb = &d; pb->print_i(); }
This yields:
1 inside Base 2 inside Derived
Even though pb was declared a pointer to type Base, it may point to a Derived object. When it does, Derived's member function is chosen.
Abstract Classes In the employee example, the base
class “makes sense”, that is, we can conceive of objects of that type.
Sometimes this is not the case. class Shape {public: virtual void rotate(int) { error(“Shape:rotate"); } virtual void draw() { error(“Shape::draw"); }};
Pure Virtual Functions Making a shape of this unspecified kind
is rather pointless, since every operation on this object results in an error.
We can get the compiler to help us keep track by making Shape an abstract class.
This is done by defining one or more of its member functions as pure virtual.
Pure Virtual Functions
Now no objects of type Shape may be created.
An abstract class can only be used as a base class for derived types.
class Shape { // ... public: virtual void rotate(int) = 0; virtual void draw() = 0; // ... };
Pure Virtual Functions
Let’s Use This Stuff!
Some classes to represent arithmetic expressions: Term (abstract) Constant : public Term BinaryOp : public Term (abstract) Plus : public BinaryOp
Expression Trees
+
+
2.21.1
3.3
Term
Variablename
Constantvalue
Expressionoperator
Binary Unary
operands This is the “Composite”design pattern
Starting the Inheritance Hierarchy
#include <iostream>#include <sstream>#include <string>using namespace std;
class Term {public: Term() {} virtual ~Term() {} virtual string symbolicEval() = 0; virtual double numericalEval() = 0;};
Our basic abstract class.It has two pure virtualmethods, and a virtualdestructor.
symbolicEval() writesan expression like((1.1 + 2.2) + 3.3)
numericalEval() evaluatesit: 6.6
The Constant Class
class Constant : public Term { double value;public: Constant() { value = 0; } Constant(double v) { value = v; } virtual ~Constant() {} virtual string symbolicEval() { ostringstream oss; oss << value; return oss.str(); } virtual double numericalEval() { return value; }};
This class is no longerabstract, since thepure virtuals areoverridden.
symbolicEval() usesan ostringstreamobject that allows“<<-ing” into astring.
The BinaryOp Class
class BinaryOp : public Term {public: virtual ~BinaryOp() { if (lChild) delete lChild; if (rChild) delete rChild; }protected: Term * lChild, * rChild; BinaryOp(Term * l, Term * r) { lChild = l; rChild = r; }};
This is the parent class forthe binary arithmeticoperators. It centralizescommon constructionand destruction activities.
Note that this class is stillabstract.
The Expression class in theUML diagram is conceptuallynice, but not needed inhere.
Plus: A Typical Binary Operator
class Plus : public BinaryOp {public: Plus(Term * l, Term * r) : BinaryOp(l, r) {} virtual ~Plus() {} virtual string symbolicEval() { ostringstream oss; oss << "(" << lChild->symbolicEval(); oss << " + "; oss << rChild->symbolicEval() << ")"; return oss.str(); } virtual double numericalEval() { return (lChild->numericalEval() + rChild->numericalEval()); }};
A Simple Driver
void main() { Constant * c1 = new Constant(1.1); Constant * c2 = new Constant(2.2); Constant * c3 = new Constant(3.3); Plus * p1 = new Plus(c1, c2); Plus * p2 = new Plus(p1, c3); cout << p2->symbolicEval() << " = "; cout << p2->numericalEval() << endl; delete p2;}
Note that all Terms are created with new, and the tree is “heldtogether” with pointers. Pay particular attention to the waydelete works. Term’s destructor must be virtual for this to work!
More Advanced Stuff
Beware Implicit Conversions
class Base { public: virtual void foo(int) { cout << “Base::foo(int)” << endl;} virtual void foo(double) {cout << “Base::foo(double)” << endl;} //... };
class Derived : public Base { public: virtual void foo(int) {cout << “Derived::foo(int)” << endl;} //... };
Beware Implicit Conversions
void main() { Derived d; Base b, *pb = &d;
b.foo(9); // selects Base::foo(int) b.foo(9.5); // selects Base::foo(double) d.foo(9); // selects Derived::foo(int) d.foo(9.5); // selects Derived::foo(int) overriden pb->foo(9); // selects Derived::foo(int) pb->foo(9.5); // selects Base::foo(double) virtual func}
Pure Virtual Functions
A pure virtual function which is not defined in a derived class remains a pure virtual function, so that the derived class is also an abstract class.
This allows us to build implementations in stages.
Pure Virtual Functions
class X { public: virtual void f() = 0; virtual void g() = 0; }; X b; // error: declaration of object of abstract class X class Y : public X { void f(); // overrides X::f }; Y b; // error: declaration of object of abstract class Y class Z : public Y { void g(); // overrides X::g }; Z c; // this is OK