CppStory Repo

Chapter #6 - C++ Class Relationships

inheritance, composition, aggregation, using

6.0 Prologue

Object-oriented design builds classes and binds them together with class relationships. Five relations exist: inheritance, composition, aggregation, using, and friendship. All but friendship appear in most C++ programs.
Quick Starter Example - class extensions via inheritance
C++ class inheritance has two very useful features:
  1. It builds flexible code by binding base class pointers to derived class instances. A function that accepts a base pointer accepts derived instances through that pointer.
  2. Sometimes inheritance brings in the base class implementation for use in a derived class. That is the approach here.
Here, we extend the std::string class to add new features, e.g.: class stringEx : public std::string, private StrUtils { ... } stringEx derives publicly from std::string to inherit its public interface, and privately from StrUtils to hide that struct's interface 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++ provides two facilities for building flexible code: inheritance and templates. This chapter covers inheritance and the other class relationships. Templates appear in Chapter 7. Conclusion: Incorporating standard C++ library functionality into our own classes is straightforward. That ability makes us significantly more productive - we just need to know how and when to use it.
This example constructs a StringEx class by inheriting publicly from std::string and privately from a string utilities class, providing all std::string facilities plus extensions from the utilities. The effect resembles C# extension methods but uses a different implementation. Inheriting from additional base classes continues the extensions - a pattern often called "mixin" classes, discussed in Chapter 7.

6.1 Defining Class Relationships

 
Fig 1. Class Relationships
Virtually all domain models can be represented by classes using these four relationships.
  1. Inheritance:
    Models an "is-a" relationship: a derived class specializes its base. The base class 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 share the same lifetimes and reside within the composer's memory footprint.
  3. Aggregation:
    Represents a weaker "part-of" relationship. The aggregator holds a pointer to the aggregated instance, created on the native heap. They do not share lifetimes, and the parts reside outside the aggregator's memory footprint.
  4. Using:
    A non-owning relationship. The user holds a pointer to the used instance, passed as a method argument. The used lifetime is independent of the user but must overlap the user's access. Used resides outside the user's memory footprint.

6.2 Object Layout

Fig 2. Object Layout
Fig 2. shows a compound object defined by classes B, C, D, and U. The corresponding objects appear in the bottom half of the figure. Two dimensions clarify the presentation, but the objects are simply segments in the process's virtual address space. Several things to note about the object layout:
  1. class B composes C, so C lies inside B's memory footprint.
  2. class D derives publicly from B, so B's memory footprint lies inside D's.
  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 atop the objects represent their public member functions. D shares all of B's member functions, unaffected by access specification, and may declare additional public functions.
Consequences of this object structure are:
  1. D's constructor must construct its inner B, usually by explicitly invoking a B constructor in its initialization sequence.
  2. When D constructs B, B's first action is to construct its inner C in its initialization sequence.
  3. D's construction is independent of the lifetime of u ε U, so the design must ensure u is in a valid state when D invokes its methods.

6.3 Compound Object Layout

The demonstration code in the details dropdown below matches the structure shown in Fig 2. Each class instance displays its memory footprint when say() 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
The demonstration output shows instance memory start and end points as in Table 1. This data is consistent with the layout properties shown in Fig. 1. Inclusion of bases and members is a fundamental part of the C++ value type object model.

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 with a size of 28 bytes. The Base instance holds a composed instance plus a Virtual Function Pointer Table (VFPT) pointer of 4 bytes. The Derived instance holds its inner Base plus its VFPT pointer, placed in the slot the inner Base provides. The code shows that each enclosing class has constructors with initialization sequences. When Derived is constructed, it calls the Base constructor in its initialization sequence. When the inner Base is constructed, it calls the Composed constructor. Similarly, Composed 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"; } Copy constructors, copy assignment operators, and destructors need no implementation here - the compiler-generated value methods are correct. Base's Composed data member is a std::string with proper value methods, and Base itself has correct compiler-generated value methods. Destructors are implemented for each class to announce destruction. Otherwise the compiler-generated destructors would suffice.

6.4 Inheritance, Run-Time Polymorphism, and Virtual Dispatching

Inheritance provides a 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()
Suppose 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. g(pBase* ptr) dispatches based on the runtime type of the object, not the static type of ptr. g needs only the public interface of Base - it knows nothing about the Base class hierarchy. Adding a new class derived from Base requires no changes to g - it processes the new class correctly. This behavior, named Liskov Substitution, is fundamental to polymorphic design.
Liskov Substitution Barbara Liskov authored "Data Abstraction," providing a model for polymorphism, paraphrased here 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. Within the function, any method invoked through the base pointer dispatches 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 uses virtual function pointer tables. Each class with one or more virtual methods has an associated Virtual Function Pointer Table (VFPT), and each instance holds a pVtbl pointer to its VFPT. Fig 3. shows a class hierarchy with base class B and derived class D. Comparing declarations:
  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 who is public but non-virtual, D inherits it - clients of D can call it, but D should not override it.
  5. A derived class holds an image of its base within its memory footprint. Here, D holds B's virtual function table pointer slot - pointing to D's VFPT - along with B::name. D 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

Chapter 1 introduced the Person class hierarchy for a quick look at class relationships. Figure 4. shows that diagram. The example is not production-quality code, but it demonstrates all class relationships except friendship, modeling a software development 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: dev ε Dev and projMgr ε ProjMgr are SW_Engs, and teamLead ε TeamLead is a Dev. Aggregation provides a temporary ownership relationship: projMgr ε ProjMgr owns project ε Project temporarily - that manager may own another Project later. Composition is permanent: Projects always hold a budget ε Budget but hold a baseline ε Baseline only temporarily. A new project starts with no code and no documents. Notice how effectively these four relationships model the workings of a software development organization.
The block at right shows a code fragment from the PeopleHierarchy example below, with using declarations for ProjectName, TeamName, Team, ProjectStaff, and Project.
showTeam receives a std::tuple containing a team name and a vector of SW_Eng* pointers. The range-based for extracts individual pointers to team members as SW_Engs. The simplicity of this function comes from Liskov Substitution: pSweng->nameAndTitle() dispatches correctly for each concrete type, e.g., TeamLead and Dev. Inheritance with Liskov Substitution builds flexible code effectively. Adding a new SW_Eng - say, QA - requires no changes to showTeam or showProject because they are ignorant of the specialized types.
All code for this example is in the details dropdown below. Study it carefully to understand how it works. Download the code from the CppStory Repository to examine and run it alongside the example.
People Hierarchy 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
The next section examines inheritance hierarchies in working code for a Parser used for static code analysis.

6.6 Example - CppParser

Fig 5. Rule-based Parser
Parsing discovers and classifies the parts of some complex structure. This section targets computer languages - specifically C, C++, Java, and C#. Parsing involves syntactic analysis, either formal reduction using a representation like BNF or an ad-hoc process. Many reasons exist to parse source code beyond compilation. For example:
  • Building code analysis tools
  • Searching for content in or ownership of code files
  • Evaluating code metrics
  • Compiling "little embedded languages"
I built this prototype to illustrate design ideas for a graduate class and to support a code analysis project assignment. The parser had to be simple enough for students to understand and incorporate into their projects within a week or two. All code for Parser is provided in CppParser repository. Parser uses an ad-hoc rule-based structure based on the Strategy Pattern1. It holds a container of IRule pointers bound to derived rules. While running, it collects a token sequence - a semi-expression - from the scanner and passes it to each rule in turn, continuing until the scanner is exhausted. Parser knows nothing about the input token sequences or how rules use them; it simply 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 detects a grammar construct by checking whether a semi-expression matches. Each rule holds a collection of IAction-derived actions. When a rule matches, it invokes each action with the matching semi-expression - an example of the Command Pattern1. Rules know nothing about the actions or the Parser. Each action operates on the semi-expression and changes the Repository state, often building an abstract syntax tree. Actions know nothing about the rules that invoke them; they need only the Repository's state structure. This design has many parts: roughly a dozen rules, each with one or more actions, plus a Tokenizer and semi-expression handler. configParser creates and owns these parts; it derives from IBuilder - an example of the Builder Pattern1. The Tokenizer uses the State Pattern1. It extracts words from a stream, handling many special cases. Toker returns quoted strings and comments as single tokens, classifying characters as alphanumeric, whitespace, or punctuators. A few punctuators return as single-character tokens for their language significance: semi-colons ";", and braces "{" and "}". All Tokenizer states derive from abstract ConsumeState, each representing a specialized form of character consumption: EatAlphanum, EatWhitespace, EatPunctuator, EatCppComment. Ten derived states handle characters in specialized ways. The State Pattern divides the special processing rules effectively. This example demonstrates rich use of inheritance hierarchies: Rules, Actions, and Tokenizer states. Adding new rules to Parser, new actions to Rules, or new States to Tokenizer requires no changes to the rest of the Parser code. This Parser has served several doctoral research projects and many graduate classes. It is an effective tool 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:
    Specializes a base type. Passing pointers or references to a base instance to a function lets that function accept any derived class. Adding a new derived class requires no changes to the function.
  2. Composition:
    Factors complex processing into a class with composed members, each handling a small cohesive part of the computation. That simplifies building, debugging, testing, and reading.
  3. Aggregation:
    Has the same utility as composition for aggregates constructed dynamically - either because initialization information is unavailable at compile time or because the execution path may not need them. Dynamic memory allocation incurs a performance penalty.
  4. Using:
    Lets a class use facilities it does not own - perhaps because they are shared or must be created by another entity that holds the required information.
  5. Friend:
    Friendship increases the scope of class encapsulation, so use it sparingly. Sometimes no alternative exists - for example, when a friend cannot be a class member for technical reasons but its functionality is required.
Two powerful approaches produce flexible code. Dynamic polymorphism uses class inheritance hierarchies, as demonstrated in this chapter. Static polymorphism uses templates, as the next chapter shows.

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. Provide void, copy construction, move construction, copy assignment, move assignment, and destructor methods.
    Assume Widget instances have correct copy, move, and destruction semantics. No specific Widget functionality is required. Consider using annunciating constructors to show WidgetUser manages its Widget correctly.
  2. Write a class that composes a fundamental type and an STL container (the specific type does not matter). Show by testing that the class correctly allows the compiler to generate its default and copy constructors, assignment operator, and destructor. Replace the composition with aggregation, using pointers to the native heap. Show that incorrect operations occur without copy, assignment, and destruction operations. Add those operations and show that class operations are valid again. Finally, prevent the compiler from generating those methods using =delete. Note that a destructor is still required (why?).
    Careful completion of this exercise is the best way to learn how to handle compiler-generated methods.
  3. Write a program that counts the number of directories rooted at some specified path, using the 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, this time evaluating the total size of all files in the directories on the specified path.
    Include the sizes of files in the root directory.
  5. Modify code in the TextFinder repository to display N lines of code surrounding matched text.
    Cache the last N/2 lines while searching, then look ahead N/2 lines when a match is found. A circular buffer is one way to implement the caching. See Exercise 4:1 for a template circular buffer - templates are not required here.

6.9 References

cpppatterns.com
Posts on Fluent C++
C++ Idioms
C++ weekly videos