A C++ Class Designer's Checklist
Steven J. Zeil
Video Recording
These slides (slightly reformatted) accompany a recorded video lecture.
Or click the icon on the opening page to view the expanded text version.
1 The Checklist
-
Is the interface complete?
-
Are there redundant functions in the interface that could be removed? Are there functions that could be generalized?
-
Have you used names that are meaningful in the application area?
-
Are pre-conditions and assumptions well documented? Have you used
assert
to “guard” the inputs? -
Are the data members private?
-
Does every constructor initialize every data member?
-
Have you appropriately treated the default constructor?
-
Have you appropriately treated the “big 3” (copy constructor, assignment operator, and destructor)?
-
Does your assignment operator handle self-assignment?
-
Does your class provide
==
and<{
} operators? -
Does your class provide an output routine?
-
Is your class const-correct?
Purpose
This is a checklist
-
Use it whenever you have the responsibility of designing an ADT interface
-
These are not absolute rules, but are things that you need to think about
- Violate them if necessary, but only after careful consideration
2 Discussion and Explanation
2.1 Is the interface complete?
An ADT interface is complete if it contains all the operations required to implement the application at hand (and/or reasonably probable applications in the near future).
The best way to determine this is to look to the requirements of the application.
Is Day complete?
class Day
{
/**
Represents a day with a given year, month, and day
of the Gregorian calendar. The Gregorian calendar
replaced the Julian calendar beginning on
October 15, 1582
*/
public:
Day(int aYear, int aMonth, int aDate);
/**
Returns the year of this day
@return the year
*/
int getYear() const;
/**
Returns the month of this day
@return the month
*/
int getMonth() const;
/**
Returns the day of the month of this day
@return the day of the month
*/
int getDate() const;
/**
Returns a day that is a certain number of days away from
this day
@param n the number of days, can be negative
@return a day that is n days away from this one
*/
Day addDays(int n) const;
/**
Returns the number of days between this day and another
day
@param other the other day
@return the number of days that this day is away from
the other (>0 if this day comes later)
*/
int daysFrom(Day other) const;
bool comesBefore (Day other) const;
bool sameDay (Day other) const;
private:
int daysSinceStart; // start is 10/15/1582
/* alternate implem:
int theYear;
int theMonth;
int theDate;
*/
};
For example, if we were to look through proposed applications of the Day
class and find designs with pseudocode like:
if (d is last day in its month)
payday = d;
we would be happy with the ability of our Day
interface to support this.
Is Day complete? (2)
On the other hand, if we encountered a design like this:
while (payday falls on the weekend)
move payday back one day
end while
we might want to consider adding a function to the Day
class to get the day of the week.
2.2 Are there redundant functions or functions that can be generalized?
-
Avoid unneded redundancies that make your class larger and provide additional code to maintain.
-
Generalize when possible to provide more options to the application.
Example: Day output
class Day
{
public:
⋮
void print() const;
private:
⋮
};
Future applications may need to send their output to different places. So, it makes sense to make the print destination a parameter:
void print (std::ostream& out);
Day output op
Of course, most C++ programmers are used to doing output this way:
cout << variable;
rather than
variable.print (cout);
So we would do better to add the operator …
Day output op
** Missing file: dayoutput.h **
2.3 Have you used names that are meaningful in the application domain?
An important part of this question is the “in the application domain”.
-
Good variable names should make sense to anyone who understands the application domain,
-
even if they don’t understand (yet) how your program works.
Example: Book identifiers
-
getTitle
,putTitle
, orgetNumberOfAuthors
are all fine. -
Not everyone who has worked with books would understand
getIdentifier
-
Better would be to recognize that suitable unique identifiers already exist
** Missing file: bookisbn.h **
* the ISBN appears on the copyright page of every published book.
2.4 Preconditions and Assertions
Are pre-conditions and assumptions well documented?
A pre-condition is a condition that the person calling a function must be sure is true, before the call, if he/she expects the function to do anything reasonable.
- Pre-conditions must be documented because they are an obligation upon the caller
- And callers can’t fulfill that obligation if they don’t know about it
Example: Day Constructor
class Day
{
/**
Represents a day with a given year, month, and day
of the Gregorian calendar. The Gregorian calendar
replaced the Julian calendar beginning on
October 15, 1582
*/
public:
Day(int aYear, int aMonth, int aDate);
/**
Returns the year of this day
@return the year
*/
int getYear() const;
/**
Returns the month of this day
@return the month
*/
int getMonth() const;
/**
Returns the day of the month of this day
@return the day of the month
*/
int getDate() const;
/**
Returns a day that is a certain number of days away from
this day
@param n the number of days, can be negative
@return a day that is n days away from this one
*/
Day addDays(int n) const;
/**
Returns the number of days between this day and another
day
@param other the other day
@return the number of days that this day is away from
the other (>0 if this day comes later)
*/
int daysFrom(Day other) const;
bool comesBefore (Day other) const;
bool sameDay (Day other) const;
private:
int daysSinceStart; // start is 10/15/1582
/* alternate implem:
int theYear;
int theMonth;
int theDate;
*/
};
What pre-condition would you impose upon the Day constructor?
- A fairly obvious requirement is for the month and day to be valid.
Day(int aYear, int aMonth, int aDate);
//pre: (aMonth > 0 && aMonth <= 12)
// && (aDate > 0 && aDate <= daysInMonth(aMonth,aYear))
-
All pre-conditions are, by definition, boolean expressions
-
As we will see shortly, there’s a significant advantage to writing them as proper C++ expressions
Example: Day Constructor (cont.)
This comment
class Day
{
/**
Represents a day with a given year, month, and day
of the Gregorian calendar. The Gregorian calendar
replaced the Julian calendar beginning on
October 15, 1582
*/
⋮
suggests a more rigorous pre-condition
Day(int aYear, int aMonth, int aDate);
//pre: (aMonth > 0 && aMonth <= 12)
// && (aDate > 0 && aDate <= daysInMonth(aMonth,aYear))
// && (aYear > 1582 || (aYear == 1582 && aMonth > 10)
// || (aYear == 1582 && aMonth == 10 && aDate >= 15)
Example: MailingList getContact
#ifndef MAILINGLIST_H
#define MAILINGLIST_H
#include <iostream>
#include <string>
#include "contact.h"
/**
A collection of names and addresses
*/
class MailingList
{
public:
MailingList();
MailingList(const MailingList&);
~MailingList();
const MailingList& operator= (const MailingList&);
// Add a new contact to the list
void addContact (const Contact& contact);
// Remove one matching contact
void removeContact (const Contact&);
void removeContact (const Name&);
// Find and retrieve contacts
bool contains (const Name& name) const;
Contact& getContact (const Name& name) const;
// combine two mailing lists
void merge (const MailingList& otherList);
// How many contacts in list?
int size() const;
bool operator== (const MailingList& right) const;
bool operator< (const MailingList& right) const;
private:
struct ML_Node {
Contact contact;
ML_Node* next;
ML_Node (const Contact& c, ML_Node* nxt)
: contact(c), next(nxt)
{}
};
int theSize;
ML_Node* first;
ML_Node* last;
// helper functions
void clear();
void remove (ML_Node* previous, ML_Node* current);
friend std::ostream& operator<< (std::ostream& out, const MailingList& addr);
};
// print list, sorted by Contact
std::ostream& operator<< (std::ostream& out, const MailingList& list);
#endif
What pre-condition, if any, would you write for the getContact
function?
Have you used assert
to “guard” the inputs?
An assert
statement takes a single argument,
-
a boolean condition that we believe should be true unless someone somewhere has made a programming mistake.
-
It aborts program execution if that condition evaluates as false.
- Can be “turned off” in release versions by a simple compiler switch
- Though that might not be a good thing …
- Can be “turned off” in release versions by a simple compiler switch
Example: Guarding the Day Constructor
#include "day.h"
#include <cassert>
using namespace std;
Day::Day(int aYear, int aMonth, int aDate)
//pre: (aMonth > 0 && aMonth <= 12)
// && (aDate > 0 && aDate <= daysInMonth(aMonth,aYear))
// && (aYear > 1582 || (aYear == 1582 && aMonth > 10)
// || (aYear == 1582 && aMonth == 10 && aDate >= 15)
{
assert (aMonth > 0 && aMonth <= 12);
assert (aDate > 0 && aDate <= 31);
assert (aYear > 1582 || (aYear == 1582 && aMonth > 10)
|| (aYear == 1582 && aMonth == 10 && aDate >= 15));
daysSinceStart = ...
}
Example: guarding getContact
** Missing file: guardGetContact.cpp **
Do Assertions Reduce Robustness?
Why do
assert (current != NULL
&& name == current->contact.getName());
instead of making the code take corrective action? E.g.,
if (current != NULL
&& name == current->contact.getName())
return current->contact;
else
return Contact();
Do Assertions Reduce Robustness?
-
Many people argue in favor of making code “tolerate” errors.
- The ability to recover from errors is called robustness.
- Robust handling of human interface errors is a good thing
-
However, tolerance of pre-conditions is reckless.
- A pre-condition violation is evidence of a bug in the application.
- If the application is under test, it’s self-defeating to hide mistakes.
- If application has been released, hiding bugs can lead to plusible but incorrect output, corrupted files & databases, etc.
2.5 Are the data members private?
As discussed earlier, we strongly favor the use of encapsulated private
data to provide information hiding in ADT implementations.
- E.g., permitting alternate implementations of the Day class
Providing Access to Attributes
Two common styles in C++:
** Missing file: attributes.cpp **
-
getAttr
,setAttr
as used for the Address attribute -
Attribute reference functions
Attribute Reference Functions
class Contact {
⋮
const Address& getAddress() const {return theAddress;}
Address& getAddress() {return theAddress;}
Attribute reference functions can be used to both access and assign to attributes
void foo (Contact& c1, const Contact& c2)
{
c1.name() = c2.name();
}
but may offer less flexibility to the ADT implementor.
- Consequently, not used as often as get/set
2.6 Does every constructor initialize every data member?
Simple enough to check, but can prevent some very difficult-to-catch errors.
Example: MailingList
Check this …
class MailingList
{
⋮
private:
struct ML_Node {
Contact contact;
ML_Node* next;
ML_Node (const Contact& c, ML_Node* nxt)
: contact(c), next(nxt)
{}
};
int theSize;
ML_Node* first;
ML_Node* last;
};
against this:
#include <cassert>
#include <iostream>
#include <string>
#include <utility>
#include "mailinglist.h"
using namespace std;
using namespace rel_ops;
MailingList::MailingList()
: first(NULL), last(NULL), theSize(0)
{}
MailingList::MailingList(const MailingList& ml)
: first(NULL), last(NULL), theSize(0)
{
for (ML_Node* current = ml.first; current != NULL;
current = current->next)
addContact(current->contact);
}
MailingList::~MailingList()
{
clear();
}
const MailingList& MailingList::operator= (const MailingList& ml)
{
if (this != &ml)
{
clear();
for (ML_Node* current = ml.first; current != NULL;
current = current->next)
addContact(current->contact);
}
return *this;
}
// Add a new contact to the list
void MailingList::addContact (const Contact& contact)
{
if (first == NULL)
{ // add to empty list
first = last = new ML_Node(contact, NULL);
theSize = 1;
}
else if (contact > last->contact)
{ // add to end of non-empty list
last->next = new ML_Node(contact, NULL);
last = last->next;
++theSize;
}
else if (contact < first->contact)
{ // add to front of non-empty list
first = new ML_Node(contact, first);
++theSize;
}
else
{ // search for place to insert
ML_Node* previous = first;
ML_Node* current = first->next;
assert (current != NULL);
while (contact < current->contact)
{
previous = current;
current = current->next;
assert (current != NULL);
}
previous->next = new ML_Node(contact, current);
++theSize;
}
}
// Remove one matching contact
void MailingList::removeContact (const Contact& contact)
{
ML_Node* previous = NULL;
ML_Node* current = first;
while (current != NULL && contact > current->contact)
{
previous = current;
current = current->next;
}
if (current != NULL && contact == current->contact)
remove (previous, current);
}
void MailingList::removeContact (const Name& name)
{
ML_Node* previous = NULL;
ML_Node* current = first;
while (current != NULL
&& name > current->contact.getName())
{
previous = current;
current = current->next;
}
if (current != NULL
&& name == current->contact.getName())
remove (previous, current);
}
// Find and retrieve contacts
bool MailingList::contains (const Name& name) const
{
ML_Node* current = first;
while (current != NULL
&& name > current->contact.getName())
{
previous = current;
current = current->next;
}
return (current != NULL
&& name == current->contact.getName());
}
Contact MailingList::getContact (const Name& name) const
{
ML_Node* current = first;
while (current != NULL
&& name > current->contact.getName())
{
previous = current;
current = current->next;
}
if (current != NULL
&& name == current->contact.getName())
return current->contact;
else
return Contact();
}
// combine two mailing lists
void MailingList::merge (const MailingList& anotherList)
{
// For a quick merge, we will loop around, checking the
// first item in each list, and always copying the smaller
// of the two items into result
MailingList result;
ML_Node* thisList = first;
const ML_Node* otherList = anotherList.first;
while (thisList != NULL and otherList != NULL)
{
if (thisList->contact < otherList->contact)
{
result.addContact(thisList->contact);
thisList = thisList->next;
}
else
{
result.addContact(otherList->contact);
otherList = otherList->next;
}
}
// Now, one of the two lists has been entirely copied.
// The other might still have stuff to copy. So we just copy
// any remaining items from the two lists. Note that one of these
// two loops will execute zero times.
while (thisList != NULL)
{
result.addContact(thisList->contact);
thisList = thisList->next;
}
while (otherList != NULL)
{
result.addContact(otherList->contact);
otherList = otherList->next;
}
// Now result contains the merged list. Transfer that into this list.
clear();
first = result.first;
last = result.last;
theSize = result.theSize;
result.first = result.last = NULL;
result.theSize = 0;
}
// How many contacts in list?
int MailingList::size() const
{
return theSize;
}
bool MailingList::operator== (const MailingList& right) const
{
if (theSize != right.theSize) // (easy test first!)
return false;
else
{
const ML_Node* thisList = first;
const ML_Node* otherList = right.first;
while (thisList != NULL)
{
if (thisList->contact != otherList->contact)
return false;
thisList = thisList->next;
otherList = otherList->next;
}
return true;
}
}
bool MailingList::operator< (const MailingList& right) const
{
if (theSize < right.theSize)
return true;
else
{
const ML_Node* thisList = first;
const ML_Node* otherList = right.first;
while (thisList != NULL)
{
if (thisList->contact < otherList->contact)
return true;
else if (thisList->contact > otherList->contact)
return false;
thisList = thisList->next;
otherList = otherList->next;
}
return false;
}
}
// helper functions
void MailingList::clear()
{
ML_Node* current = first;
while (current != NULL)
{
ML_Node* next = current->next;
delete current;
current = next;
}
first = last = NULL;
theSize = 0;
}
void MailingList::remove (MailingList::ML_Node* previous,
MailingList::ML_Node* current)
{
if (previous == NULL)
{ // remove front of list
first = current->next;
if (last == current)
last = NULL;
delete current;
}
else if (current == last)
{ // remove end of list
last = previous;
last->next = NULL;
delete current;
}
else
{ // remove interior node
previous->next = current->next;
delete current;
}
--theSize;
}
// print list, sorted by Contact
std::ostream& operator<< (std::ostream& out, const MailingList& list)
{
MailingList::ML_Node* current = list.first;
while (current != NULL)
{
out << current->contact << "\n";
current = current->next;
}
out << flush;
return out;
}
book1.setTitle(''bogus title'');
assert (book1.getTitle() == ''bogus title'');
book2 = book1;
assert (book1 == book2);
book1.setTitle(''bogus title 2'');
assert (! (book1 == book2));
catalog.add(book1);
assert (catalog.firstBook() == book1);>
string s1, s2;
cin >> s1 >> s2;
if (s1 < s2) ''abc'' < ''def''
''abc'' < ''abcd''
x y
Exactly one of the following is true for any x and y
x == y
x < y
y < x
namespace std{
namespace relops {
template <class T>
bool operator!= (T left, T right)
{
return !(left == right);
}
template <class T>
bool operator> (T left, T right)
{
return (right < left);
}
using namespace std::relops;
2.7 Have you appropriately treated the default constructor?
Remember that the default constructor is a constructor that can be called with no arguments.
Your options are:
1.The compiler-generated version is acceptable. 2. Write your own 3. No default constructor is appropriate 4. (very rare) If you don’t want to allow other code to construct objects of your ADT type at all, declare a constructor and make it private
.
2.8 Have you appropriately treated the “Big 3”?
-
Recall that the Big 3 in C++ class design are the copy constructor, the assignment operator, the destructor.
-
For each of these, if you do not provide them, the compiler generates a version for you.
-
-
The Rule of the Big 3 states that,
if you provide your own version of any one of the big 3, you should provide your own version of all 3.
Handling the Big 3
So your choices as a class designer come down to:
-
The compiler-generated version is acceptable for all three.
-
You have provided your own version of all three.
-
You don’t want to allow copying of this ADT’s objects
-
Provide private versions of the copy constructor and assignment operator so the compiler won’t provide public ones, but no one can use them.
The Compiler-Generated Versions are wrong when…
- Copy constructor
- Shallow-copy is inappropriate for your ADT
- Assignment operator
- Shallow-copy is inappropriate for your ADT
- Destructor
- Your ADT holds resources that need to be released when no longer needed
The Compiler-Generated Versions are wrong when… (2)
Generally this occurs when
-
Your ADT has pointers among its data members, and
-
You don’t want to share the objects being pointed to.
The Rule of the Big 3
The Rule of the Big 3 states that,
if you provide your own version of any one of the big 3, you should provide your own version of all 3.
Why? Because we don’t trust the compiler-generated …
- … copy constructor
- if our data members include pointers to data we don’t share
- … assignment operator
- if our data members include pointers to data we don’t share
- …
- destructor if our data members include pointers to data we don’t share
So if we don’t trust one, we don’t trust any of them.
2.9 Does your assignment operator handle self-assignment?
If we assign something to itself:
x = x;
we normally expect that nothing really happens.
But when we are writing our own assignment operators, that’s not always the case.
Sometimes assignment of an object to itself is a nasty special case that breaks thing badly.
2.10 Does your class provide ==
and <
operators?
-
The compiler never generates these implicitly, so if we want them, we have to supply them.
-
The
==
and<
are often required if you want to put your objects inside other data structures.- That’s enough reason to provide them whenever practical.
-
Also heavily used in test drivers
2.11 Does your class provide an output routine?
- Even if not required by the application, useful (essential?) in testing and debugging.
- Again, that’s enough reason to always provide one if at all practical to do so
2.12 Is your class const-correct?
In C++, we use the keyword const
to declare constants. But it also has two other important uses:
-
indicating what formal parameters a function will look at, but promises not to change
-
indicating which member functions don’t change the object they are applied to
These last two uses are important for a number of reasons
-
This information often helps make it easier for programmers to understand the expected behavior of a function.
-
The compiler may be able to use this information to generate more efficient code.
-
This information allows the compiler to detect many potential programming mistakes.
Const Correctness
A class is const-correct if
-
Any formal function parameter that will not be changed by the function is passed by copy or as a const reference (
const &
). -
Every member function that does not alter the object it’s applied to is declared as a const member.
Example: Contact
Passed by copy, passed by const ref, & const member functions
#ifndef CONTACT_H
#define CONTACT_H
#include <iostream>
#include <string>
#include "name.h"
#include "address.h"
class Contact {
Name theName;
Address theAddress;
public:
Contact (Name nm, Address addr)
: theName(nm), theAddress(addr)
{}
Name getName() const {return theName;}
void setName (Name nm) {theName= nm;}
Address getAddress() const {return theAddress;}
void setAddress (Address addr) {theAddress = addr;}
bool operator== (const Contact& right) const
{
return theName == right.theName
&& theAddress == right.theAddress;
}
bool operator< (const Contact& right) const
{
return (theName < right.theName)
|| (theName == right.theName
&& theAddress < right.theAddress);
}
};
inline
std::ostream& operator<< (std::ostream& out, const Contact& c)
{
out << c.getName() << " @ " << c.getAddress();
return out;
}
#endif
Const Correctness Prevents Errors
- This code will not compile:
void foo (Contact& conOut, const Contact& conIn)
{
conIn = conOut;
}
- nor will this:
void bar (Contact& conOut, const Contact& conIn)
{
conIn.setName (conOut.getName());;
}
- The error messages aren’t always easy to understand (“discards qualifier”, “no match for … ”)
- Resist the temptation to strip away the “const”s or use type-casting to bypass the errors.
Example: MailingList
Passed by copy, passed by const ref, & const member functions
#ifndef MAILINGLIST_H
#define MAILINGLIST_H
#include <iostream>
#include <string>
#include "contact.h"
/**
A collection of names and addresses
*/
class MailingList
{
public:
MailingList();
MailingList(const MailingList&);
~MailingList();
const MailingList& operator= (const MailingList&);
// Add a new contact to the list
void addContact (const Contact& contact);
// Remove one matching contact
void removeContact (const Contact&);
void removeContact (const Name&);
// Find and retrieve contacts
bool contains (const Name& name) const;
Contact getContact (const Name& name) const;
// combine two mailing lists
void merge (const MailingList& otherList);
// How many contacts in list?
int size() const;
bool operator== (const MailingList& right) const;
bool operator< (const MailingList& right) const;
private:
struct ML_Node {
Contact contact;
ML_Node* next;
ML_Node (const Contact& c, ML_Node* nxt)
: contact(c), next(nxt)
{}
};
int theSize;
ML_Node* first;
ML_Node* last;
// helper functions
void clear();
void remove (ML_Node* previous, ML_Node* current);
friend std::ostream& operator<< (std::ostream& out, const MailingList& addr);
};
// print list, sorted by Contact
std::ostream& operator<< (std::ostream& out, const MailingList& list);
#endif
3 Summary
This is a checklist
-
Use it whenever you have the responsibility of designing an ADT interface
-
These are not absolute rules, but are things that you need to think about
- Violate them if necessary, but only after careful consideration