about
10/24/2022
C++ Story Class Relationships
C++ Story Code

Chapter #6 - C++ Class Relationships

inheritance, composition, aggregation, using

6.0 Prologue

Object-oriented design and implementation consists of constructing classes and binding them together with class relationships. There are five relations: inheritance, composition, aggregation, using, and friendship. Of these, all but the last are found in many implementations of C++ programs.
Quick Starter Example - class extensions via inheritance
C++ class inheritance has two very useful features:
  1. It supports building flexible code by allowing base class pointers to bind to derived class instances. So a function that accepts a base pointer can accept derived instances through that pointer.
  2. Occasionally we use inheritance just to use the base class implementation in a derived class. That's what we do in this example.
Here, we extend the std::string class to add new features, e.g.: class stringEx : public std::string, private StrUtils { ... } stringEx derives publically from std::string to inherit its public interface. It derives privately from StrUtils to hide the interface of that struct while using its methods internally.
Code: Inheriting from std::string struct StrUtils { /*----------------------------------------- remove whitespace from front and back of string argument - does not remove newlines */ std::string trim(const std::string& toTrim) { if (toTrim.size() == 0) return toTrim; std::string temp; std::locale loc; typename std::string::const_iterator iter = toTrim.begin(); while ( isspace(*iter, loc) && *iter != '\n' ) { if (++iter == toTrim.end()) { break; } } for (; iter != toTrim.end(); ++iter) { temp += *iter; } typename std::string::reverse_iterator riter; size_t pos = temp.size(); for ( riter = temp.rbegin(); riter != temp.rend(); ++riter ) { --pos; if ( !isspace(*riter, loc) || *riter == '\n' ) { break; } } if (0 <= pos && pos < temp.size()) temp.erase(++pos); return temp; } /*----------------------------------------- split sentinel separated strings into a vector of trimmed strings */ template <typename T> std::vector<std::string> split( const std::string& toSplit, T splitOn = ',' ) { std::vector<std::string> splits; std::string temp; typename std::string::const_iterator iter; for ( iter = toSplit.begin(); iter != toSplit.end(); ++iter ) { if (*iter != splitOn) { temp += *iter; } else { splits.push_back(trim(temp)); temp.clear(); } } if (temp.length() > 0) splits.push_back(trim(temp)); return splits; } }; /*------------------------------------------- super string */ class stringEx : public std::string, private StrUtils { public: stringEx() {} stringEx(const std::string& str) : std::string(str) {} stringEx(const char* pStr) : std::string(pStr) {} std::string trim() { StrUtils::trim(*this); } std::vector<std::string> splits(char splitOn = ',') { return StrUtils::split(*this, splitOn); } }; /*--- show collection of string splits ----*/ void showSplits( const std::vector<std::string>& splits, std::ostream& out = std::cout ) { out << "\n"; for (auto item : splits) { if (item == "\n") out << "\n--" << "newline"; else out << "\n--" << item; } out << "\n"; } Using Code using Splits = std::vector<std::string>; int main() { displayDemo("-- SuperString demo --\n"); std::string arg = "one, "; arg += "this is two, "; arg += "and finally three"; stringEx superStr{ arg }; std::cout << "\n superStr has the value: " << superStr; Splits splits = superStr.splits(); std::cout << "\n superStr splits are:"; for (auto split : splits) { std::cout << "\n " << split; } putline(2); } Output -- SuperString demo -- superStr has the value: one, this is two, and finally three superStr splits are: one this is two and finally three C++ has two facilities that are very useful for building flexible code: inheritance and templates. We discuss inheritance and the other class relationships in this chapter. Templates come in Chapter 6. Conclusion: Its relatively easy to incorporate functionality from the standard C++ libraries in our own classes. That ability makes us significantly more productive. We just have to know how and when to use it. Cheers!
This example constructs a StringEx class by inheriting publically from std::string and privately from a string utilities class. The effect is to provide all of the facilities of the std::string to StringEx along with extensions provided by the utilities. The effect is similar to C# extension methods, but implemented in a different way. It would be easy to continue extensions by inheriting from additional base classes. That is often referred to as use of "mixin" classes. We will talk in more detail about them in Chapter 7.

6.1 Defining Class Relationships

 
Fig 1. Class Relationships
It is surprising, that virtually all domain models can be represented by classes using these four relationships.
  1. Inheritance:
    Models an "is-a" relationship, e.g., a derived class is a specialization of its base. The base class is always an inner part of the derived - it resides within the memory footprint of the derived class.
  2. Composition:
    Represents a strong "part-of" relationship. The composer always contains its composed parts. They have the same life-times, and the parts reside within the memory footprint of the composer.
  3. Aggregation:
    Represents a weaker "part-of" relationship. The aggregator holds a reference to the aggregated, e.g., usually a pointer to an instance of the aggregated, created by the aggregator, in the native heap. They do not have the same life-times, and the parts reside outside the memory footprint of their aggregator.
  4. Using:
    This is a non-owning relationship. The user holds a reference to the used, e.g., a pointer to an instance of the used, passed as an argument of a user method. The used lifetime is independent of the user, but for correctness, it must be concurrent with the user's access. Used resides outside the memory footprint of the user.

6.2 Object Layout

Fig 2. Object Layout
In Fig 2. we show a compound object, defined by the classes B, C, D, and U. The corresponding objects are shown in the bottom half of the Figure. They are shown using two dimensions for clarity of presentations, but are actually simply segments in a section of the process's virtual address space. There are several things to note about the object layout:
  1. class B composes C with the result that C lies inside the memory footprint of B.
  2. class D derives publically from B and B's memory footprint lies entirely inside that of D.
  3. D uses an instance of class U so U's footprint is disjoint from that of D.
  4. Client aggregates an instance of D and their footprints are disjoint.
  5. A friend class has access to D's private members but the footprints of friend and D are disjoint.
  6. The bumps on the top of the objects represent their public member functions. D shares all the member functions of B (unaffected by access specification) but may also declare new public functions.
Consequences of this object structure are:
  1. When D's constructor is called, it must construct its inner B, usually by explicitly invoking a B constructor in its initialization sequence - more about that later.
  2. When B is constructed by D its first action is to construct its inner C, usually in its initialization sequence.
  3. D's construction is independent of the lifetime of u ε U. That means that our design must insure that u is in a valid state when D invokes its methods.

6.3 Compound Object Layout

Demonstration code in the details dropdown, below, has the same structure as shown in Fig 2. Each class instance displays its memory footprint on the console when its say() method is called.
Demo - Class Layout
Class Layout Code ///////////////////////////////////////////// // Used class is used by Derived, shows // its statistics class Used { public: Used(const std::string& msg) : msg_(msg) { std::cout << "\n Used(const std::string&) called"; } ~Used() { std::cout << "\n ~Used() called"; } void say() { std::cout << "\n\n Used::say()"; showStatistics(this); showString("Used", msg_); } private: std::string msg_; }; ///////////////////////////////////////////// // Composed class is a data member of Base, // shows its statistics class Composed { public: Composed(const std::string& msg) : msg_(msg) { std::cout << "\n Composed(const std::string&) called"; } ~Composed() { std::cout << "\n ~Composed() called"; } void say() { std::cout << "\n\n Composed::say()"; showStatistics(this); showString("Composed", msg_); } private: std::string msg_; }; ///////////////////////////////////////////// // Base class holds Composed and displays // its layout statistics class Base { public: Base(const std::string& msg) : composed_(msg) { std::cout << "\n Base(const std::string&) called"; } // If you remove virtual qualifier on ~Base() // only Base destructor is called in main if // created on heap. virtual ~Base() { std::cout << "\n ~Base() called"; } virtual void say() { std::cout << "\n\n Base::say()"; showStatistics(this); std::cout << "\n\n Base invoking Composed::say(): "; composed_.say(); } protected: Composed composed_; }; ///////////////////////////////////////////// // Derived inherits from Base and displays // its layout statistics. class Derived : public Base { public: Derived(const std::string& msg) : Base(msg) { std::cout << "\n Derived(const std::string&) called"; } ~Derived() { std::cout << "\n ~Derived() called"; } virtual void say(Used& used) { std::cout << "\n\n Derived::say()"; showStatistics(this); std::cout << "\n\n Derived calling Base::say(): "; Base::say(); std::cout << std::endl; std::cout << "\n\n Derived calling Used::say(): "; used.say(); std::cout << std::endl; } private: }; Using Code: std::string arg = "string entered as constructor argument"; std::cout << "\n \"" << arg << "\""; std::cout << "\n It's object size is " << sizeof(arg) << " bytes"; std::cout << "\n It contains " << arg.size() << " characters\n"; std::cout << "\n creating used object on stack"; std::cout << "\n -------------------------------"; Used u(arg); u.say(); std::cout << std::endl; std::cout << "\n creating base object on stack"; std::cout << "\n -------------------------------"; Base b(arg); b.say(); std::cout << std::endl; ///////////////////////////////////////////// // Works same whether Derived created on // stack or heap, but you will notice // differences in the region of memory occupied std::cout << "\n creating derived object on stack"; std::cout << "\n ----------------------------------"; Derived d(arg); d.say(u); std::cout << "\n creating derived object on heap"; std::cout << "\n ---------------------------------"; Base* pB = new Derived(arg); pB->say(); delete pB; Output: creating derived object on stack ---------------------------------- Composed(const std::string&) called Base(const std::string&) called Derived(const std::string&) called Derived::say() class Derived my size is: 32 bytes -- holds Base and Composed my starting address is 6420740 (0x61F904) my ending address is 6420772 (0x61F924) Derived calling Base::say(): Base::say() class Derived my size is: 32 bytes -- holds string and ptr to vtbl my starting address is 6420740 (0x61F904) my ending address is 6420772 (0x61F924) Base invoking my Composed::say(): Composed::say() class Composed my size is: 28 bytes -- holds string my starting address is 6420744 (0x61F908) my ending address is 6420772 (0x61F924) "This Composed string entered as ctor argument" has 54 characters Derived calling Used::say(): Used::say() class Used my size is: 28 bytes my starting address is 6420820 (0x61F954) my ending address is 6420848 (0x61F970) "This Used string entered as constructor argument" has 50 characters creating derived object on heap --------------------------------- Composed(const std::string&) called Base(const std::string&) called Derived(const std::string&) called Base::say() class Derived my size is: 32 bytes -- holds string and ptr to vtbl my starting address is 12524472 (0xBF1BB8) my ending address is 12524504 (0xBF1BD8) Base invoking my Composed::say(): Composed::say() class Composed my size is: 28 bytes -- holds string my starting address is 12524476 (0xBF1BBC) my ending address is 12524504 (0xBF1BD8) "This Composed string entered as ctor arg" has 54 characters
Looking at the demonstration output we see instance memory start and end points as shown in Table 1. This data is consistant with the layout properties shown in Fig. 1. This inclusion of bases and members is a fundamental part of the object model for C++ value types.

Table 1. - Memory Footprints of Class Layout Instances

class start end size parts
Derived 2096532 (0x1FFD94) 2096564 (0x1FFDB4) 32 bytes Base (32 bytes - including slot for VFPT pointer)
Base 2096532 (0x1FFD94) 2096564 (0x1FFDB4) 32 bytes Composed (28 bytes) + VFPT pointer
Composed 2096536 (0x1FFD98) 2096564 (0x1FFDB4) 28 bytes std::string (28 bytes)
Used 2096612 (0x1FFDE4) 2096640 (0x1FFE00) 28 bytes std::string (28 bytes)
The Composed class instance holds a std::string which has a size of 28 bytes. The Base instance holds a composed instance plus a Virtual Function Pointer Table (VFPT) pointer which is 4 bytes. The Derived instance holds its inner Base instance plus its VFPT pointer, placed in the slot its inner Base provides. If you look at the code you will see that each including class has constructors with initialization sequences. Here's code snippets that show them. When the Derived class is constructed it calls the Base constructor for its inner Base part in its initialization sequence. When the Base inner instance is constructed it calls the Composed constructor on its inner composed part. Similarly, the Composed part constructs its inner std::string. Constructor Initialization Sequences Derived(const std::string& msg) : Base(msg) { std::cout << "\n Derived(const std::string&) called"; } Base(const std::string& msg) : composed_(msg) { std::cout << "\n Base(const std::string&) called"; } Composed(const std::string& msg) : msg_(msg) { std::cout << "\n Composed(const std::string&) called"; } We didn't have to implement copy constructors, copy assignment operators, and destructors because the compiler generated value methods are correct. The Composed data member of Base is a std::string which has the value methods. And the Base from which Derived inherits has correct compiler generated value methods. We did implement destructors for each class to announce when each instance was destroyed. Otherwise, we could have allowed the compiler to generate those too.

6.4 Inheritance, Run-Time Polymorphism, and Virtual Dispatching

Inheritance relationships afford a very powerful sharing and reuse mechanism through substitution. For any Base-Derived relationship: class Derived : public Base { ... }; Derived class instances have the properties:
  1. Any Derived instance can be bound to a Base pointer: Derived d;
    Base* pBase = &d;
  2. Any virtual function in the Base class can be redefined in the Derived class: class Base { public: virtual void f() { ... }; ... }; class Derived : public Base { public: void f() override { ... } ... };
  3. When dispatching calls: If pBase is bound to a Base instance, pBase->f() calls Base::f()
    If pBase is bound to a Derived instance, pBase->f() calls Derived::f()
Now, suppose that we define a function: void g(pBase* ptr) { ptr->f(); } If ptr is bound to a Base instance then Base::f() is called. If ptr is bound to a Derived instance, then Derived::f() is called. So g(pBase* ptr) invokes functions based on the type of the object reference, not the static type of ptr. Function g doesn't need to know anything about the Base class hierarchy. It only needs to know the public interface of the Base class. At any time, if we add a new class derived from Base, then g, with no changes, will process it correctly. This is a very important behavior. So important that it has been given a name: Liskov Substitution.
Liskov Substitution Barbara Liskov authored a paper "Data Abstraction" in which she provided a useful model for polymorphism, which I've paraphrased in a way appropriate for C++: Functions that accept pointers or C++ references statically typed to some base class must be able to use objects of classes derived from the base through those pointers or references without any knowledge specialized to the derived classes. That means that within the function any method invocation made through the base pointer will invoke that method based on the type of the derived class instance bound to that pointer.

6.4.1 Virtual Dispatching via Virtual Function Pointer Table

Fig 3. Virtual Function Pointer Tables
Virtual function dispatching, as described above, is implemented with virtual function pointer tables. Each class with one or more virtual methods has an associated Virtual Function Pointer Table (VFPT) and each instance of that class contains a pointer, pVtbl, pointing to its VFPT. In Fig 3., we've shown a class hierarchy with base class B and derived class D. Comparing the class declarations we see that:
  1. D does not override B::mf1, so its virtual function pointer, pMf1, binds to B::mf1
  2. D does override B::mf2, so its virtual function pointer, pMf2, binds to D::mf2
  3. D adds a new virtual function, mf3. This function cannot be invoked from a B* pointer, because it is not part of the public B interface.
  4. D overrides the private method B::name. That function is called only by the non-virtual B::who. Since that function is public, but non-virtual, D inherits who, so clients of D can call it, but D should not change it (because it's non-virtual).
  5. Remember that a derived class holds an image of its base within its memory footprint. We see that here, as the D image holds B's virtual function table pointer slot, which it uses to hold a pointer to its own VFPT, and B::name. To that it adds its own member data: D::name.
The details dropdown, below, presents code for this demonstration and its output.
Polymorphism Demo Demo Code class B { public: B(const std::string& name); virtual ~B() {} virtual void mf1(); virtual void mf2(); void who(); private: virtual std::string name(); std::string name_; }; B::B(const std::string& name) : name_(name) {} void B::mf1() { std::cout << "\n B::mf1() invoked"; } void B::mf2() { std::cout << "\n B::mf2() invoked"; } std::string B::name() { return name_; } void B::who() { std::cout << "\n who returns name " << this->name(); } class D : public B { public: D(const std::string& name); virtual ~D() {} virtual void mf2() override; virtual void mf3(); private: virtual std::string name() override; std::string name_; }; D::D(const std::string& name) : B("B"), name_(name) {} void D::mf2() { std::cout << "\n D::mf2() invoked"; } void D::mf3() { std::cout << "\n D::mf3() invoked"; } std::string D::name() { return name_; } Using Code displayDemo("--- polymorphism demo ---"); displayDemo("\n create B and invoke its methods"); B b{ "B" }; b.who(); b.mf1(); b.mf2(); displayDemo("\n create D and invoke its methods"); D d{ "D" }; d.who(); d.mf1(); d.mf2(); d.mf3(); displayDemo( "\n create B* pB = &b, and invoke methods" ); B* pB = &b; pB->who(); pB->mf1(); pB->mf2(); displayDemo( "\n create B* pB = &d, and invoke methods" ); pB = &d; pB->who(); pB->mf1(); pB->mf2(); //pB->mf3(); //won't compile: mf2 not in base interface D* pD = dynamic_cast<D*>(pB); if (pD) pD->mf3(); Output --- polymorphism demo --- create B and invoke its methods who returns name B B::mf1() invoked B::mf2() invoked create D and invoke its methods who returns name D B::mf1() invoked D::mf2() invoked D::mf3() invoked create B* pB = &b, and invoke methods who returns name B B::mf1() invoked B::mf2() invoked create B* pB = &d, and invoke methods who returns name D B::mf1() invoked D::mf2() invoked D::mf3() invoked

6.5 Example - Person Class Hierarchy

You may recall, from Chapter1 - Survey, the Person class hierarchy, used for a quick look at class relationships. Figure 4. shows the diagram we used there. The example isn't very useful for professional code, but it makes a nice example of the class relationships. All of them, with the exception of friendship, are used here to build an effective model for a software developement project organization.
Fig 4. Person Class Hierarchy
The classes are:
  • IPerson: An interface for the Person class.
  • Person: derives from IPerson Class that provides attributes: name, occupation, and age for all the other concrete classes.
  • ISW_Eng: Interface for all people who develop software.
  • SW-Eng: derives from Person and ISW_Eng Abstract class that shares select code and a Person reference with all software engineer classes.
  • Dev: derives from SW_Eng and uses Baseline Class that represents software developers - those who focus on building and verifying software components.
  • TestLead: derives from Dev and uses Baseline Class for people who lead software development teams and also develop software.
  • ProgMgr: derives from SW_Eng and aggregates Project Represents people who manage software development product teams.
  • Project: composes Budget and aggregates Baseline Collection of Project Manager, TeamLeads, Devs that also includes Budget and Baseline.
  • Budget: Class holding original budget, current budget, projected empty date.
  • BaseLine: Class holding collections of code modules and documents.
Liskov Substitution at work: using ProjectName = std::string; using TeamName = std::string; using Team = std::pair<TeamName, std::vector<SW_Eng*>> using ProjectStaff = std::vector<Team> using Project = std::tuple<ProjectName, ProjMgr, ProjectStaff> //-------------------------------------------------------------------- // defined in SW_Eng // pPer_ is a pointer to the SW_Eng inner Person std::string SW_Eng::nameAndTitle() { return pPer_->name() + ", " + pPer_->occupation(); } //-------------------------------------------------------------------- void showTeam(Team& team) { std::cout << "\n Team " << team.first; for (auto pSweng : team.second) std::cout << "\n " << pSweng->nameAndTitle(); } void showProject(Project& prj) { auto [prjName, prjMgr, staff] = prj; std::cout << "\n " << prjName; std::cout << "\n " << prjMgr.nameAndTitle(); for (auto team : staff) { showTeam(team); } }
Inheritance provides an "is-a" relationship, so dev ε Dev and projMgr ε ProjMgr are SW_Engs, and a teamLead ε TeamLead is a Dev. Aggregation provides a temporary owner relationship, so projMgr ε ProjMgr owns, temporarily the project ε Project instance. Some time later that manager may own another Project. Composition is a permanent owner relationship. Projects always have a budget ε Budget, but only temporarily have a baseline ε Baseline. When a project starts it has no code and no documents. Notice how effectively these four relationships model the workings of a software development organization.
In the block at right we show a code fragment taken from the PeopleHierarchy example, below. There are using declarations for ProjectName, TeamName, Team, ProjectStaff, and Project.
The showTeam function is passed a std::tuple representing team name and a vector of pointers to the abstract SW_Eng class. The range-based for is extracting individual pointers to each of the team members, as SW_Engs. The simplicity of this function comes from Liskov Substitution. pSweng->nameAndTitle() calls that function as if called by each of the appropriate types, e.g., TeamLead and Dev. Using inheritance with Liskov Substitution is one very useful way of building flexible code. If we chose to add another SW_Eng, say QA, nothing would change in any of the other code because it is ignorant of the specialized types we are using, e.g., look at showTeam and showProject again.
All of the code for this example is contained in the details dropdown, below. The example is worth a significant amount of your time to understand how it does what it does. If you go to the CppStory Repository and download that code, you can look at the example below and simultaneously look at and run the repository code.
People Hierachy Code Example Person Interface and Header Code ///////////////////////////////////////////////////////////// // IPerson.h - declares interface for inner person // // // // Jim Fawcett, Teaching Professor Emeritus, Syracuse Univ // ///////////////////////////////////////////////////////////// #include <string> #include <memory> namespace Chap4 { struct IPerson { using Name = std::string; using Occupation = std::string; using Age = int; using Stats = std::tuple<Name, Occupation, Age>; virtual ~IPerson() {} virtual Stats stats() const = 0; virtual void stats(const Stats& sts) = 0; virtual Name name() const = 0; virtual Occupation occupation() const = 0; virtual void occupation(const Occupation& occup) = 0; virtual Age age() const = 0; virtual void age(const Age& ag) = 0; virtual bool isValid() const = 0; }; std::unique_ptr<IPerson> createPerson(const IPerson::Stats& stats); } ///////////////////////////////////////////////////////////// // Person.h - defines inner person attributes // // // // Jim Fawcett, Teaching Professor Emeritus, Syracuse Univ // ///////////////////////////////////////////////////////////// #include "IPerson.h" namespace Chap4 { class Person : public IPerson { public: virtual ~Person(); Person(); Person(const Stats& sts); virtual Stats stats() const; virtual void stats(const Stats& sts); virtual Name name() const; virtual Occupation occupation() const; virtual void occupation(const Occupation& occup); virtual Age age() const; virtual void age(const Age& ag); virtual bool isValid() const; private: Stats personStats; }; } SW_Eng Interface and Header ///////////////////////////////////////////////////////////// // ISW_Eng.h - defines interface for all SW Eng's // // // // Jim Fawcett, Teaching Professor Emeritus, Syracuse Univ // ///////////////////////////////////////////////////////////// #include "IPerson.h" namespace Chap4 { struct ISW_Eng { virtual ~ISW_Eng() {} virtual void doWork() = 0; virtual void attendMeeting() = 0; virtual IPerson* person() = 0; }; } ///////////////////////////////////////////////////////////// // SW_Eng.h - defines attributes for all SW Eng's // // // // Jim Fawcett, Teaching Professor Emeritus, Syracuse Univ // ///////////////////////////////////////////////////////////// #include "Person.h" #include "ISW_Eng.h" #include <string> #include <memory> namespace Chap4 { class SW_Eng : public ISW_Eng, public Person { public: SW_Eng() {} SW_Eng(IPerson::Stats stats); virtual ~SW_Eng() {} virtual void doWork() = 0; virtual void attendMeeting() = 0; virtual IPerson* person(); std::string nameAndTitle(); protected: void getCoffee(); void checkEmail(); void developSoftware(); void reviewTeamActivities(); void performanceAppraisals(); void introductions(const std::string& name); void presentStatus(const std::string& progress); void assignActionItems(); IPerson* pPer_ = nullptr; }; } Dev Header ///////////////////////////////////////////////////////////// // Dev.h - defines attributes for developer // // // // Jim Fawcett, Teaching Professor Emeritus, Syracuse Univ // ///////////////////////////////////////////////////////////// #include "SW_Eng.h" #include <iostream> namespace Chap4 { class Dev : public SW_Eng { public: Dev(IPerson::Stats stats); virtual void doWork(); virtual void attendMeeting(); private: //IPerson* pPer_; }; } TeamLead Header ///////////////////////////////////////////////////////////// // TeamLead.h - defines attributes for Team Leaders // // // // Jim Fawcett, Teaching Professor Emeritus, Syracuse Univ // ///////////////////////////////////////////////////////////// #include "Dev.h" #include <iostream> namespace Chap4 { class TeamLead : public Dev { public: TeamLead(IPerson::Stats stats); virtual void doWork(); virtual void attendMeeting(); private: //IPerson* pPer_; }; } ProjMgr Header ///////////////////////////////////////////////////////////// // ProjMgr.h - defines attributes for Project Managers // // // // Jim Fawcett, Teaching Professor Emeritus, Syracuse Univ // ///////////////////////////////////////////////////////////// #include "SW_Eng.h" #include <iostream> namespace Chap4 { class ProjMgr : public SW_Eng { public: ProjMgr(IPerson::Stats stats); virtual void doWork(); virtual void attendMeeting(); private: //IPerson* pPer_; }; } Using Code ///////////////////////////////////////////////////////////// // TestPeopleHierarchy.cpp - demonstrates hierarchy // // // // Jim Fawcett, Teaching Professor Emeritus, Syracuse Univ // ///////////////////////////////////////////////////////////// #ifdef TEST_HIERARCHY #include <vector> #include "SW_Eng.h" #include "Dev.h" #include "TeamLead.h" #include "ProjMgr.h" namespace Chap4 { using ProjectName = std::string; using TeamName = std::string; using Team = std::pair<TeamName, std::vector<SW_Eng*>> using ProjectStaff = std::vector<Team> using Project = std::tuple<ProjectName, ProjMgr, ProjectStaff> void showTeam(Team& team) { std::cout << "\n Team " << team.first; for (auto pSweng : team.second) std::cout << "\n " << pSweng->nameAndTitle(); } void showProject(Project& prj) { auto [prjName, prjMgr, staff] = prj; std::cout << "\n " << prjName; std::cout << "\n " << prjMgr.nameAndTitle(); for (auto team : staff) { showTeam(team); } } } int main() { using namespace Chap4; ProjMgr Devin({ "Devin", "Project Manager", 45 }); TeamLead Jill({ "Jill", "Team Lead & Web dev", 32 }); Dev Jack({ "Jack", "UI dev", 28 }); Dev Zhang({ "Zhang", "System dev", 37 }); Dev Charley({ "Charley", "QA dev", 27 }); Team FrontEnd{ "FrontEnd", { &Jill, &Jack, &Zhang, &Charley } }; TeamLead Tom({ "Tom", "Team Lead & Backend Dev", 38 }); Dev Ming({ "Ming", "Comm dev", 26 }); Dev Sonal({ "Sonal", "Server dev", 27 }); Team BackEnd{ "BackEnd", { &Tom, &Ming, &Sonal } }; Project ProductX{ "ProductX", Devin, { FrontEnd, BackEnd } }; showProject(ProductX); std::cout << std::endl; std::cout << "\n Team " << FrontEnd.first << " at work"; for (auto pDev : FrontEnd.second) { pDev->doWork(); pDev->attendMeeting(); } std::cout << std::endl; std::cout << "\n Project Manager " << Devin.nameAndTitle() << " at work"; Devin.doWork(); Devin.attendMeeting(); std::cout << "\n\n"; } #endif Person Implementation Code ///////////////////////////////////////////////////////////// // Person.cpp - defines inner person attributes // // // // Jim Fawcett, Teaching Professor Emeritus, Syracuse Univ // ///////////////////////////////////////////////////////////// #include "IPerson.h" #include <tuple> #include <string> #include <iostream> #include "Person.h" namespace Chap4 { Person::Person() {} Person::Person(const Stats& sts) { personStats = sts; } Person::~Person() {} Person::Stats Person::stats() const { return personStats; } void Person::stats(const Stats& sts) { personStats = sts; } Person::Name Person::name() const { return std::get<0>(personStats); } Person::Occupation Person::occupation() const { return std::get<1>(personStats); } void Person::occupation(const Occupation& occup) { std::get<1>(personStats) = occup; } Person::Age Person::age() const { return std::get<2>(personStats); } void Person::age(const Age& ag) { std::get<2>(personStats) = ag; } bool Person::isValid() const { return name() != "" && age() >= 0; } std::unique_ptr<IPerson> createPerson(const IPerson::Stats& stats) { return std::move(std::make_unique<Person>(*new Person(stats))); } template<typename P> void displayPerson(const P& person) { std::cout << "\n " << person.name() << ", " << person.age() << ", " << person.occupation(); } template<typename P> void displayInvalid(const P& person) { std::cout << "\n " << person.name() << " has invalid data"; } template<typename P> void checkedDisplay(const P& person) { displayPerson(person); if (!person.isValid()) displayInvalid(person); } } SW_Eng Implementation ///////////////////////////////////////////////////////////// // SW_Eng.cpp - defines attributes for all SW Eng's // // // // Jim Fawcett, Teaching Professor Emeritus, Syracuse Univ // ///////////////////////////////////////////////////////////// #include "SW_Eng.h" #include "Person.h" #include <iostream> #include <string> namespace Chap4 { /*--- initialize inner person ---*/ SW_Eng::SW_Eng(IPerson::Stats stats) { Person::stats(stats); pPer_ = person(); } /*--- return inner person ---*/ IPerson* SW_Eng::person() { return dynamic_cast<IPerson*>(this); } std::string SW_Eng::nameAndTitle() { return pPer_->name() + ", " + pPer_->occupation(); } void SW_Eng::getCoffee() { std::cout << "\n go for coffee, chat with friends"; } void SW_Eng::checkEmail() { std::cout << "\n open mail and slog through messages"; } void SW_Eng::developSoftware() { std::cout << "\n pull current work from repository"; std::cout << "\n chase bugs"; std::cout << "\n design new module"; std::cout << "\n start module implementation"; std::cout << "\n push current work to repository"; } void SW_Eng::reviewTeamActivities() { std::cout << "\n review current work status"; std::cout << "\n review individual's accomplishments"; } void SW_Eng::performanceAppraisals() { std::cout << "\n record individual accomplishments"; std::cout << "\n summarize areas needing improvement"; std::cout << "\n summarize contributions to the team"; } void SW_Eng::introductions(const std::string& name) { std::cout << "\n Hi everyone, my name is " << name << " and I'm pleased to see you all"; } void SW_Eng::presentStatus(const std::string& progress) { std::cout << "\n I'm happy to report that " << progress; } void SW_Eng::assignActionItems() { std::cout << "\n I will post action items " << "for you before the end of the day"; } } Dev Implementation ///////////////////////////////////////////////////////////// // Dev.cpp - defines attributes for developer // // // // Jim Fawcett, Teaching Professor Emeritus, Syracuse Univ // ///////////////////////////////////////////////////////////// #include "Dev.h" namespace Chap4 { Dev::Dev(IPerson::Stats stats) : SW_Eng(stats) {} void Dev::doWork() { std::cout << "\n " << pPer_->name() << " starting work"; getCoffee(); checkEmail(); developSoftware(); std::cout << std::endl; } void Dev::attendMeeting() { std::cout << "\n " << pPer_->name() << " attending meeting"; introductions(pPer_->name()); presentStatus( "I've completed 90% of my assigned tasks for this sprint" ); std::cout << std::endl; } } TeamLead Implementation ///////////////////////////////////////////////////////////// // TeamLead.cpp - defines attributes for Team Leaders // // // // Jim Fawcett, Teaching Professor Emeritus, Syracuse Univ // ///////////////////////////////////////////////////////////// #include "TeamLead.h" namespace Chap4 { TeamLead::TeamLead(IPerson::Stats stats) : Dev(stats) {} void TeamLead::doWork() { std::cout << "\n " << nameAndTitle() << ", starting work"; getCoffee(); checkEmail(); reviewTeamActivities(); developSoftware(); std::cout << std::endl; } void TeamLead::attendMeeting() { std::cout << "\n " << nameAndTitle() << ", attending meeting"; introductions(pPer_->name()); presentStatus( "we' completed 95% of assigned stories for this sprint" ); std::cout << std::endl; } } ProjMgr Implementation ///////////////////////////////////////////////////////////// // ProjMgr.cpp - defines attributes for Project Managers // // // // Jim Fawcett, Teaching Professor Emeritus, Syracuse Univ // ///////////////////////////////////////////////////////////// #include "ProjMgr.h" namespace Chap4 { ProjMgr::ProjMgr(IPerson::Stats stats) : SW_Eng(stats) {} void ProjMgr::doWork() { std::cout    "\n "    nameAndTitle()    ", starting work"; getCoffee(); checkEmail(); reviewTeamActivities(); performanceAppraisals(); std::cout << std::endl; } void ProjMgr::attendMeeting() { std::cout    "\n "    nameAndTitle()    ", attending meeting"; introductions(pPer_->name()); presentStatus( "My teams completed 85% of assigned work for this sprint" ); std::cout    "\n Take customer golfing"; std::cout    std::endl; } } Output ProductX Devin, Project Manager Team FrontEnd Jill, Team Lead & Web dev Jack, UI dev Zhang, System dev Charley, QA dev Team BackEnd Tom, Team Lead & Backend Dev Ming, Comm dev Sonal, Server dev Team FrontEnd at work Jill, Team Lead & Web dev, starting work go for coffee, chat with friends open mail and slog through messages review current work status review individual's accomplishments pull current work from repository chase bugs design new module start module implementation push current work to repository Jill, Team Lead & Web dev, attending meeting Hi everyone, my name is Jill and I'm pleased to see you all I'm happy to report that our team has completed 95% of assigned stories for this sprint Jack starting work go for coffee, chat with friends open mail and slog through messages pull current work from repository chase bugs design new module start module implementation push current work to repository Jack attending meeting Hi everyone, my name is Jack and I'm pleased to see you all I'm happy to report that I've completed 90% of assigned tasks for this sprint Zhang starting work go for coffee, chat with friends open mail and slog through messages pull current work from repository chase bugs design new module start module implementation push current work to repository Zhang attending meeting Hi everyone, my name is Zhang and I'm pleased to see you all I'm happy to report that I've completed 90% of assigned tasks for this sprint Charley starting work go for coffee, chat with friends open mail and slog through messages pull current work from repository chase bugs design new module start module implementation push current work to repository Charley attending meeting Hi everyone, my name is Charley and I'm pleased to see you all I'm happy to report that I've completed 90% of assigned tasks for this sprint Project Manager Devin, Project Manager at work Devin, Project Manager, starting work go for coffee, chat with friends open mail and slog through messages review current work status review individual's accomplishments record individual accomplishments summarize areas needing improvement summarize contributions to the team Devin, Project Manager, attending meeting Hi everyone, my name is Devin and I'm pleased to see you all I'm happy to report that My teams have completed 85% of assigned work for this sprint Take customer golfing
In the next section we will look at inheritance hierarchies from working code for a Parser used for static code analysis.

6.6 Example - CppParser

Fig 5. Rule-based Parser
Parsing is the process of discovering and classifying the parts of some complex thing. Our interests are in parsing computer languages and particularly C, C++, Java, and C#. In this context parsing is the process of some form of syntactic analysis, which may be based on a formal reduction using some representation like BNF, or using an Ad-Hoc process. There are a lot of reasons you may wish to parse source code beyond compiling it's text. For example:
  • Building code analysis tools
  • Searching for content in or ownership of code files
  • Evaluating code metrics
  • Compiling "little embedded languages"
Several years ago I built a protype to illustrate some design ideas for one of my graduate classes and to serve as help for a code analysis project that I wanted to assign to them. The parser had to be simple enough and easy enough to use for students to understand it and incorporate successfully into their projects in a week or two and still get the project assignment turned in on time. All code for Parser is provided in CppParser repository. Parser uses an ad-hoc rule-based structure, based on the Strategy Pattern1. Parser holds a container of IRule pointers that are bound to derived rules. While running it collects a token sequence, called a semi-expression, from the scanner and passes that to each rule in turn. It continues that process until there are no more token collections to extract from the scanner. Really simple - Parser doesn't know anything about the input token sequences and doesn't know how rules will use them. It is simply a traffic cop that gives rules what they need to do their job.
Selected Parser Code Parser Code class IBuilder { public: virtual ~IBuilder() {} virtual Parser* Build() = 0; }; /////////////////////////////////////////////// // abstract base class for parsing actions // - when a rule succeeds, it invokes any // registered action class IAction { public: virtual ~IAction() {} virtual void doAction( const Scanner::ITokCollection* pTc ) = 0; }; /////////////////////////////////////////////// // abstract base class for parser language // construct detections // - rules are registered with parser for use class IRule { public: static const bool Continue = true; static const bool Stop = false; virtual ~IRule() {} void addAction(IAction* pAction); void doActions(const Scanner::ITokCollection* pTc); virtual bool doTest( const Scanner::ITokCollection* pTc ) = 0; protected: std::vector<IAction*> actions; }; class Parser { public: Parser(Scanner::ITokCollection* pTokCollection); ~Parser(); void addRule(IRule* pRule); bool parse(); bool next(); private: Scanner::ITokCollection* pTokColl; std::vector<IRule*> rules; }; //----< parse SemiExp by applying all rules to it >-------- bool Parser::parse() { for (size_t i = 0; i<rules.size(); ++i) { std::string debug = pTokColl->show(); bool doWhat = rules[i]->doTest(pTokColl); if (doWhat == IRule::Stop) break; } return true; } Tokenizer Code class ConsumeState; // private worker class struct Context; // private shared data storage class Toker { public: Toker(); Toker(const Toker&) = delete; ~Toker(); Toker& operator=(const Toker&) = delete; bool attach(std::istream* pIn); std::string getTok(); bool canRead(); void returnComments(bool doReturnComments = true); bool isComment(const std::string& tok); size_t currentLineCount(); void setSpecialTokens( const std::string& commaSeparatedString ); private: ConsumeState* pConsumer; Context* _pContext; }; class ConsumeState { friend class Toker; public: using Token = std::string; ConsumeState(); ConsumeState(const ConsumeState&) = delete; ConsumeState& operator=( const ConsumeState& ) = delete; virtual ~ConsumeState(); void attach(std::istream* pIn); virtual void eatChars() = 0; void consumeChars() { _pContext->_pState->eatChars(); _pContext->_pState = nextState(); } bool canRead() { return _pContext->_pIn->good(); } std::string getTok() { return _pContext->token; } bool hasTok() { return _pContext->token.size() > 0; } ConsumeState* nextState(); void returnComments(bool doReturnComments = false); size_t currentLineCount(); void setSpecialTokens( const std::string& commaSeparatedString ); void setContext(Context* pContext); protected: Context* _pContext; bool collectChar(); bool isOneCharToken(Token tok); bool isTwoCharToken(Token tok); Token makeString(int ch); }; struct Context { Context(); ~Context(); std::string token; std::istream* _pIn; std::vector<std::string> _oneCharTokens = { "\n", "<", ">", "{", "}", "[", "]", "(", ")", ":", ";", " = ", " + ", " - ", "*", ".", ",", "@" }; std::vector<std::string> _twoCharTokens = { "<<", ">>", "::", "++", "--", "==", "+=", "-=", "*=", "/=" }; int prevChar; int currChar; bool _doReturnComments; bool inCSharpString = false; size_t _lineCount; ConsumeState* _pState; ConsumeState* _pEatCppComment; ConsumeState* _pEatCComment; ConsumeState* _pEatWhitespace; ConsumeState* _pEatPunctuator; ConsumeState* _pEatAlphanum; ConsumeState* _pEatSpecialCharacters; ConsumeState* _pEatDQString; ConsumeState* _pEatSQString; ConsumeState* _pEatRawCppString; ConsumeState* _pEatRawCSharpString; };
Each rule is a grammar construct detector. Its job is to check whether a semi-expression matches its rule. Each rule holds a collection of actions derived from IAction. When a rule matches it simply invokes each of its actions with the matching semi-expression. This is an example of the Command Pattern1. Rules are ignorant of what the actions do and know nothing about the Parser. Each action operates on the the passed semi-expression and uses the results to change the state of the Repository, often building an abstract syntax tree. Actions don't know anything about the rules that invoke them. The only thing they need to know is the structure of the Repository's state. There are quite a few parts in this design: perhaps a dozen rules, each with one or more actions, a Tokenizer, and semi-expression handler. These parts are created and owned by configParser which derives from IBuilder. This is an example of the Builder Pattern1. Finally, the Tokenizer is based on the State Pattern1. Its job is to extract words from a stream, but there are many special cases. Toker returns quoted strings and comments as single tokens. It has to classify characters as alphanumeric, white-space, and punctuators. A few of the punctuators are returned as single character tokens because of their significance to the language, e.g., semi-colons, ";", braces, "{" and "}". Tokenizer states all derive from an abstract ConsumeState. Each state represents a specialized form of consuming characters from the input stream, e.g., EatAlphanum, EatWhitespace, EatPunctuator, EatCppComment. There are ten derived consumption states, each of which has to take care of handling characters in a specialized way. The State Pattern helps us divide and conquer a lot of special processing rules. We are looking at this example because it has such a rich use of inheritance hierarchies: Rules, Actions, and Tokenizer states. Their use makes this design very flexible. We can add new rules to Parser, new actions to Rules, and new States to Tokenizer without any affect on the rest of the Parser code. This Parser has been used in several Doctoral research projects and by many of my graduate classes for a variety of projects. We've found it to be an effective facility for learning language structure in the classroom and building research tools in the lab.

  1. Patterns cited above are all discussed in " Design Patterns, Elements of Reusable Object Oriented Software " and many web tutorials and blogs.

6.7 Epilogue

Each of the class relationships has a particular mission:
  1. Inheritance:
    Provides a specialization of some base type. If we pass pointers to, or references of, a base instance to a function, the function can accept pointers or references bound to any class that derives from that base. This makes using code very flexible. If we need to add a new derived class the function won't be modified in any way.
  2. Composition:
    Allows us to factor some complex processing into a class with composed members, each of which, handle some small cohesive part of the computation. That simplifies building, debugging, testing, and enhances reading comprehension.
  3. Aggregation:
    Has the same utility as composition for aggregates that need to be constructed dynamically, perhaps because we don't have enough information to initialize at compile time, or perhaps we may elect not to create them because the execution path hasn't needed them. To do so, we pay a performance penalty for the dynamic memory allocation.
  4. Using:
    Allows a class to use facilities that it does not own, perhaps because they are shared or must be created by another entity that has the information needed to create.
  5. Friend:
    We try not to use friendship because it increases the scope of class encapsulation. There are times when there is no other alternative. For example, the friend may not be a candidate, for technical reasons, to be a class member, but its functionality is needed in the current program.
There are two powerful ways of writing flexible code. One uses dynamic polymorphism via class inheritance heirarchies, as we have demonstrated in this chapter. The other uses static polymorphism with templates, as we will see in the next chapter.

6.8 Programming Exercises - C++ basic syntax

  1. Write code for a WidgetUser class that holds a Widget* ptr to a Widget instance on the native heap. Please provide void, copy construction, move construction, copy assignment, move assignment, and destructor methods for this class.
    You may assume that Widget instances have correct copy, move, and destruction semantics. You don't need to provide any specific functionality for Widgets. Consider using annunciating constructors to show that WidgeUser is properly managing its Widget.
  2. Write code for a class that composes a fundamental type and an STL container (if doesn't really matter which). Show, by careful testing, that the class can correctly allow the compiler to generate its default and copy constructors, assignment operator, and destructor. Now replace the composition with aggregation where the fundamental and STL container types are referenced by pointers to the native heap. Show that incorrect operations occur if you don't provide the copy, assignment, and destruction operations. Now add those operations and show that class operations are valid again. Finally, prevent the compiler from generating those methods using =delete. Note that you will have to provide a destructor (why is that?).
    Doing this exercise carefully is the best way I know of for you to learn how to handle compiler generated methods.
  3. Write all the code for a program that counts the number of directories rooted at some specified path. That will require you to use an executive package and packages DirExplorerN and FileSystem.
    DirExplorerN is one of the projects in the FileManager Repository, and FileSystem is in the FileSystem Repository.
  4. Repeat the first exercise, but now evaluate the size of the root path, e.g., the size of all files that belong to any of the directories on the specified path.
    Don't forget to include the sizes of all the files in the root directory.
  5. Modify code in the TextFinder repository so you display N lines of code surrounding the matched text.
    This will require you to cache the last N/2 lines of code while searching, and look ahead N/2 lines when you find a match. A ciruclar buffer is one way to implement that caching. See Exercise 4:1 for an exercise to build a template circular buffer. You won't need templates for this buffer.

6.9 References

cpppatterns.com
Posts on Fluent C++
C++ Idioms
C++ weekly videos
  Next Prev Pages Sections About Keys