Last modified: Feb 13, 2014
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
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.
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
class Day
{
public:
⋮
void print(std::ostream out) const;
private:
⋮
};
inline
std::ostream& operator<< (std::ostream& out, Day day)
{
dat.print (out);
return out;
}
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, or getNumberOfAuthors are all fine.
Not everyone who has worked with books would understand getIdentifier
Better would be to recognize that suitable unique identifiers already exist
class Book {
public:
⋮
std::string getISBN();
⋮
};
* the ISBN appears on the copyright page of every published book.
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.
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?
Day(int aYear, int aMonth, int aDate);
//pre: (aMonth > 0 && aMonth <= 12)
// && (aDate > 0 && aDate <= daysInMonth(aMonth,aYear))
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?
// Find and retrieve contacts
bool contains (const Name& name) const;
Contact& getContact (const Name& name) const;
//pre: contains(name)
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.
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
#include "mailinglist.h"
#include <cassert>
using namespace std;
Contact& MailingList::getContact (const Name& name) const
{
ML_Node* current = first;
while (current != NULL
&& name > current->contact.getName())
{
previous = current;
current = current->next;
}
assert (current != NULL
&& name == current->contact.getName());
return current->contact;
}
⋮
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?
As discussed earlier, we strongly favor the use of encapsulated private data to provide information hiding in ADT implementations.
Providing Access to Attributes
Two common styles in C++:
class Contact {
public:
Contact (Name nm, Address addr);
const Name& name() const {return theName;}
Name& name() {return theName;}
const Address& getAddress() const {return theAddress;}
Address& getAddress() {return theAddress;}
⋮
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.
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;
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.
Recall that the Big 3 in C++ class design are the copy constructor, the assignment operator, the destructor.
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…
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 …
So if we don’t trust one, we don’t trust any of them.
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.
The compiler never generates these implicitly, so if we want them, we have to supply them.
Also heavily used in test drivers
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
void foo (Contact& conOut, const Contact& conIn)
{
conIn = conOut;
}
void bar (Contact& conOut, const Contact& conIn)
{
conIn.setName (conOut.getName());;
}
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
This is a checklist
Use it whenever you have the responsibility of designing an ADT interface