A C++ Class Design Checklist

Steven J. Zeil

Last modified: Jul 16, 2014

Contents:
1. The Checklist
2. Discussion and Explanation
2.1 Is the interface complete?
2.2 Are there redundant functions or functions that can be generalized?
2.3 Have you used names that are meaningful in the application domain?
2.4 Preconditions and Assertions
2.5 Are the data members private?
2.6 Does every constructor initialize every data member?
2.7 Have you appropriately treated the default constructor?
2.8 Have you appropriately treated the “Big 3”?
2.9 Does your assignment operator handle self-assignment?
2.10 Does your class provide == and < operators?
2.11 Does your class provide an output routine?
2.12 Is your class const-correct?
3. Summary

1. The Checklist


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?


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

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


class Day
{
public:
  ⋮
  void print(std::ostream out) const;
private:
  ⋮
};

inline
std::ostream& operator<< (std::ostream& out, Day day)
{
  dat.print (out);
  return out;
}


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

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

What pre-condition would you impose upon the Day constructor?


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

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


#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?

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++:


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;}


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

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:


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 …

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?

  1. The compiler never generates these implicitly, so if we want them, we have to supply them.

  2. 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.
  3. Also heavily used in test drivers

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:

These last two uses are important for a number of reasons


Const Correctness

A class is const-correct if


Example: Contact

Passed by copy, passed by const ref, & const member functions

contactcc.h


Const Correctness Prevents Errors


Example: MailingList

Passed by copy, passed by const ref, & const member functions

mailinglistcc.h

3. Summary


Summary

This is a checklist