Inheritance and interfaces are very powerful organizational tools for programming. You can do a lot of things with them that you cannot do easily without them. On the flip side, they should be approached with caution. Inheritance, in particular, introduces intricate sources of bugs and confusion that you avoid by not using it, and worse yet, over time, a bad decision about inheritance can make your code convoluted, inefficient, and incorrect. Therefore, if you do decide to contaminate your code with inheritance, keep it to a minimum, and document your decisions cleanly and thoroughly. I cannot stress this enough. |
Koozies:
|
Dog Sweaters:
|
You are keeping track of your sales in text files, one file per sale. Your files have the following format:
"Koozie" color monogram "Dog" color S|L
txt/f1.txt
Isabelle Human Koozie Red IH Koozie Blue BH Dog S Red |
txt/f2.txt
Jonathan Cherubim Dog L Orange Dog S White Koozie Orange JLC Koozie White JXC |
You'd like to write a program that formats each sale nicely and reports your profits and expenses. Yarn is $2.25 a yard, and Etsy charges 8% fees on your gross. Here's what you have envisioned:
UNIX> bin/make_invoice_1_normal < txt/f1.txt Customer: Isabelle Human Description Price Costs Red Koozie with monogram IH 10.00 3.38 Blue Koozie with monogram BH 10.00 3.38 Small Red Dog Sweater 20.00 6.75 Gross: 40.00 Costs: 13.50 Fees: 3.20 Profit: 23.30 UNIX> bin/make_invoice_1_normal < txt/f2.txt Customer: Jonathan Cherubim Description Price Costs Large Orange Dog Sweater 40.00 13.50 Small White Dog Sweater 20.00 6.75 Orange Koozie with monogram JLC 10.00 3.38 White Koozie with monogram JXC 10.00 3.38 Gross: 80.00 Costs: 27.00 Fees: 6.40 Profit: 46.60 UNIX>
class Dog { public: Dog(istringstream &ss); string Description() const; double Price() const; double Expenses() const; protected: string color; string size; double yards; double price; }; Dog::Dog(istringstream &ss) { if (!(ss >> size >> color)) { throw runtime_error("Bad stringstream in Dog constructor"); } if (size == "S") { yards = 3.0; price = 20.0; } else if (size == "L") { yards = 6.0; price = 40.0; } else { throw runtime_error("Bad dog size - should be S or L"); } } | string Dog::Description() const { string s; s = (size == "S") ? "Small " : "Large "; s += color; s += " Dog Sweater"; return s; } double Dog::Price() const { return price; } double Dog::Expenses() const { return yards * 2.25; } |
I've written the definition for the Koozie class in a different style -- here we implement the code in the class definition and specify default values in the constructor specification. You may prefer this style, and while I acknowledge the simplicity of the specifications of Price() and Expenses(), I still don't like them. I like to have defintions and implementations separated.
class Koozie { protected: string color; string monogram; double yards; double price; public: Koozie(istringstream &ss) : yards(1.5), price(10.0) { if (!(ss >> color >> monogram)) throw runtime_error("Bad Koozie"); }; string Description() const { string s; s = color + " Koozie with monogram "; s += monogram; return s; } double Price() const { return price; } double Expenses() const { return yards * 2.25; } }; |
The main() code is really straightforward. When it's reading the items, it reads the first word on the line and decides whether to create an instance of Dog or Koozie. I'm going to highlight some lines in this code that feel like they are more clunky and inefficient than they should be:
int main() { istringstream ss; string name; string line; string key; Dog *d; Koozie *k; string desc; // Description of an item. double p; // Price of an item. double e; // Expenses of an item. double tp; // Running total of prices. double te; // Running total of expenses. double fees; // Fees /* Read and print the customer name. */ getline(cin, name); printf("Customer: %s\n", name.c_str()); printf("\n"); /* Print labels for the items. */ printf("%-60s %6s %6s\n", "Description", "Price", "Costs"); | /* Print each item -- description, price, expenses. You'll note that there's some code duplication, because Dog and Koozie are different classes. */ tp = 0; te = 0; while (getline(cin, line)) { ss.clear(); ss.str(line); ss >> key; if (key == "Koozie") { k = new Koozie(ss); desc = k->Description(); // These three lines p = k->Price(); // feel e = k->Expenses(); // redundant. delete k; } else if (key == "Dog") { d = new Dog(ss); desc = d->Description(); // With these p = d->Price(); // three e = d->Expenses(); // lines. delete d; } tp += p; te += e; printf("%-60s %6.2lf %6.2lf\n", desc.c_str(), p, e); } /* Print the final information. */ printf("\n"); fees = tp * 0.08; printf("Gross: %8.2lf\n", tp); printf("Costs: %8.2lf\n", te); printf("Fees: %8.2lf\n", fees); printf("Profit: %8.2lf\n", tp - te - fees); return 0; } |
class Item { public: virtual ~Item() {}; virtual string Description() const = 0; virtual double Price() const = 0; virtual double Expenses() const = 0; }; |
Our Dog and Koozie classes have no changes, except they specify that they implement the Item interface. The changed code is in red. You'll note, both classes implement the exact methods specified in Item, with the same prototypes. That is required for them to implement the Item.
class Dog : public Item { public: Dog(istringstream &ss); string Description() const; double Price() const; double Expenses() const; protected: string color; string size; double yards; double price; }; | class Koozie : public Item { public: Koozie(istringstream &ss); string Description() const; double Price() const; double Expenses() const; protected: string color; string monogram; double yards; double price; }; |
In the main code, we no longer declare the Dog or Koozie pointers. Instead, we simply declare a pointer to an Item:
int main() { Item *i; // No longer do we have separate Dog / Koozie pointers. istringstream ss; ... |
And then when we create an instance of Dog or Koozie, we assign the pointer to the Item pointer. When we call the item's methods, it calls the appropriate method of Dog or Koozie. The code is simpler:
while (getline(cin, line)) { ss.clear(); ss.str(line); ss >> key; if (key == "Koozie") { // Here is the relevant code. i = new Koozie(ss); // Since Koozie/Dog implement the } else if (key == "Dog") { // Item interface, we can treat i = new Dog(ss); // them as items. } desc = i->Description(); // Now there is no code duplication. p = i->Price(); e = i->Expenses(); delete i; tp += p; te += e; printf("%-60s %6.2lf %6.2lf\n", desc.c_str(), p, e); } |
You need that destructor code in there to make deletion work. In this case, it is specifying that you can use a default destructor for the Item. You will also use the default destructor for Dog and Koozie, since you have not specified them. See the last section of these lecture notes for some explanation on destructors.
I think you can see how this functionality is powerful, and in many cases can clean up your code. When you run it, you'll see that it runs identically to bin/make_invoice_1_normal.
class Item { public: virtual ~Item() {}; virtual string Description() const = 0; // We have moved these variables and members into the class. double Price() const; double Expenses() const; protected: string color; double yards; double price; }; /* And we implement them here. */ double Item::Price() const { return price; } double Item::Expenses() const { return yards * 2.25; } |
We remove the common variables and member functions from Dog and Koozie. Let's take a quick look at the Dog specification and constructor:
/* Our Dog class now only has the items that are specific to it. */ class Dog : public Item { public: Dog(istringstream &ss); string Description() const; protected: string size; }; Dog::Dog(istringstream &ss) { if (!(ss >> size >> color)) { throw runtime_error("Bad stringstream in Dog constructor"); } if (size == "S") { yards = 3.0; price = 20.0; } else if (size == "L") { yards = 6.0; price = 40.0; } else { throw runtime_error("Bad dog size - should be S or L"); } } |
You'll notice that Dog can use the variables in Item, because it "inherits" them. That's pretty neat, isn't it? And also pretty powerful. If you want, you can create a new class that inherits Dog, and now:
Inheritance on the other hand allows you to set up an arbitrarily complex hierarchy of methods, variables and virtual methods. The main class (superclass) contains all three of these things, and then any subclass that is derived from the superclass "inherits" the methods and variables and may use them as if they belong to the superclass. As above, a pointer to a subclass may be treated as a pointer to the superclass.
You'll note that C++ doesn't have a distinction between interface and inheritance. You implement an interface when your superclass doesn't have any data or non-virtual methods.
There are many other issues involved with inheritance with respect to constructors, destructors, and public/protected/private designations. I'm not going to teach you any of that. In this lecture, I simply want to let you know what an interface is, and get your feet wet with inheritance.
Inheritance, on the other hand, is a powder keg. Sure, it's very powerful, and you can eliminate redundancy as we did with the example above. However, inheritance has two very strong negative features, the impact of which is probably not apparent when you first start to learn the concepts:
For that reason, I don't use inheritance. Now, you, gentle reader, are going to be 20+ years younger than I am, and given the prevalence of inheritance in both C++ and Python, will not have the luxury of ridding your life of it. So, learn what I've taught you here, and please try to keep a critical eye towards inheritance, and think twice before choosing an inheritance-based solution to your problems. What you gain in "simplicity" and "elegance", you will lose in documentation...
If you do write destructors, what happens is as follows: When you delete a pointer, it will call the proper subclass destructor first, and then the superclass destructor next. (You can extrapolate this down the hierarchy if you are using a hierarchy of inheritance). The following code illustrates. Simply read the inline comments to see what it does. It is in src/destruct.cpp:
/* src/destruct.cpp. James S. Plank Tue Sep 25 14:47:43 EDT 2018 This shows how destructors work with interfaces. */ #include <iostream> #include <sstream> #include <vector> #include <stdio.h> #include <stdlib.h> using namespace std; /* We'll have one superclass called Superclass. It will have no virtual methods with the exception of a destructor. */ class Superclass { public: virtual ~Superclass(); }; /* The destructor will simply print out that it is being called. */ Superclass::~Superclass() { printf("Superclass destructor.\n"); } /* Now, we have two subclasses, S1 and S2. Each of their destructors are declared as virtual, and print out that they are being called: */ class S1 : public Superclass { public: virtual ~S1(); }; S1::~S1() { printf("S1 destructor.\n"); } class S2 : public Superclass { public: virtual ~S2(); }; S2::~S2() { printf("S2 destructor.\n"); } /* The main is pretty sparse. It simply allocates instances of S1 and S2, and then deletes them. Each delete call will call the destructor of the subclass, and then the destructor of the superclass. */ int main() { Superclass *s1, *s2; s1 = new S1; s2 = new S2; delete s1; delete s2; return 0; } |
Here is the running code:
UNIX> bin/destruct S1 destructor. Superclass destructor. S2 destructor. Superclass destructor. UNIX>