A C++ Class Designer's Checklist

Steven J. Zeil

Last modified: Dec 19, 2017
Contents:

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

  1. Is the interface complete?

  2. Are there redundant functions in the interface that could be removed? Are there functions that could be generalized?

  3. Have you used names that are meaningful in the application area?

  4. Are pre-conditions and assumptions well documented? Have you used assert to “guard” the inputs?

  5. Are the data members private?

  6. Does every constructor initialize every data member?

  7. Have you appropriately treated the default constructor?

  8. Have you appropriately treated the “big 3” (copy constructor, assignment operator, and destructor)?

  9. Does your assignment operator handle self-assignment?

  10. Does your class provide == and <{} operators?

  11. Does your class provide an output routine?

  12. Is your class const-correct?


Purpose

This is a checklist

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?

day.h

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?


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”.


Example: Book identifiers




** 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.


Example: Day Constructor

day.h

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

mailinglist.h
#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,


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?

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.


Providing Access to Attributes

Two common styles in C++:




** Missing file: attributes.cpp **

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.

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:

mailinglist.cpp
#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”?

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:

  1. The compiler-generated version is acceptable for all three.

  2. You have provided your own version of all three.

  3. You don’t want to allow copying of this ADT’s objects

  4. 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


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?

2.11 Does your class provide an output routine?

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:

  1. indicating what formal parameters a function will look at, but promises not to change

  2. indicating which member functions don’t change the object they are applied to

These last two uses are important for a number of reasons


Const Correctness

A class is const-correct if

  1. Any formal function parameter that will not be changed by the function is passed by copy or as a const reference (const &).

  2. 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

contactcc.h
#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

mailinglistcc.h
#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