520 likes | 649 Views
STAT 598W: Lecture 12. Purdue University More 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).
E N D
STAT 598W: Lecture 12 Purdue University More 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 Manageris anEmployee, 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.53 Curly Howard, 366, 4, 2.03 Curly 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 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. void main() { Base b; Base* pb = &b; Derived d; b.i = 1 d.i = 2; pb->print_i(); pb = &d; pb->print_i(); }
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 class Shape { // ... public: virtual void rotate(int) = 0; virtual void draw() = 0; // ... }; • Now no objects of type Shape may be created. • An abstract class can only be used as a base class for derived types.
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 operands This is the “Composite” design pattern Term Expression Variable Constant + operator name value + 3.3 Binary Unary 1.1 2.2
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 virtual methods, and a virtual destructor. symbolicEval() writes an expression like ((1.1 + 2.2) + 3.3) numericalEval() evaluates it: 6.6
The Constant Class This class is no longer abstract, since the pure virtuals are overridden. symbolicEval() uses an ostringstream object that allows “<<-ing” into a string. 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; } };
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 for the binary arithmetic operators. It centralizes common construction and destruction activities. Note that this class is still abstract. The Expression class in the UML diagram is conceptually nice, but not needed in here.
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 “held together” with pointers. Pay particular attention to the way delete works. Term’s destructor must be virtual for this to work!
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 }