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:
-
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.
-
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.
-
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.
-
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.
-
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.
-
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
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:
-
class B composes C with the result that C lies inside the memory footprint of B.
-
class D derives publically from B and B's memory footprint lies entirely inside that of D.
-
D uses an instance of class U so U's footprint is disjoint from that of D.
-
Client aggregates an instance of D and their footprints are disjoint.
-
A friend class has access to D's private members but the footprints of friend and D are disjoint.
-
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:
-
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.
-
When B is constructed by D its first action is to construct its inner C,
usually in its initialization sequence.
-
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:
-
Any Derived instance can be bound to a Base pointer:
Derived d;
Base* pBase = &d;
-
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 { ... }
...
};
-
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:
-
D does not override B::mf1, so its virtual function pointer,
pMf1, binds to B::mf1
-
D does override B::mf2, so its virtual function pointer,
pMf2, binds to D::mf2
-
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.
-
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).
-
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
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.
6.7 Epilogue
Each of the class relationships has a particular mission:
-
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.
-
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.
-
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.
-
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.
-
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
-
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.
-
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.
-
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.
-
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.
-
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