Implementing ADTs in C++ Classes

Steven J Zeil

Last modified: Feb 13, 2014

1. Implementing ADTs in C++

An ADT is implemented by supplying

We sometimes refer to the ADT itself as the ADT specification or the ADT interface, to distinguish it from the code of the ADT implementation.

1.1 Declaring New Data Types

In the course of designing a program, we discover that we need a new data type. What are our options?

Use an Existing Type

On rare occasions, we may be lucky enough to have an existing type that provides exactly the capabilities we want for our new type.

typedef std::string CityName;

typedef basically gives us a way to attach a more convenient name to an existing type.

Build our ADT “from scratch”

bookInterface.h

Inherit from an existing class

In between those two extremes, we sometimes have an existing type that provides almost everything we want for our new type, but needs just a little more data or a few more functions to make it what we want.


class BookInSeries: public Book {
   public:
     std::string getSeriesTitle() const;
     void putSeriesTitle(std::string theSeries);

     int getVolume() const;
     void putVolume(int);
   private:
     std::string seriesTitle;
     int volume;
};


1.2 Data Members

class Book {
 public:
  std::string title;
  int numAuthors;
  std::string isbn;
  Publisher publisher;

  static const int maxAuthors = 12;
  Author* authors;  // array of Authors
};

Privacy

class Book {
 public:
    ⋮
 private:
  std::string title;
  int numAuthors;
  std::string isbn;
  Publisher publisher;

  static const int maxAuthors = 12;
  Author* authors;  // array of Authors
};

1.3 Function Members

In addition to data, we can associate selected functions with our ADTs. For example, the missing access to the book data in our prior example can be provided by such function members.

book1.h

Then, in a separate file named book.cpp we would place the function definitions (bodies).

book1.cpp

ADTs need not be complicated

None of the functions in that ADT are particularly complex.

Inline Functions

Many of the member functions in this example are simple enough that we might consider an alternate approach, declaring them as inline functions, in which case the book.h file would look something like this:

book1in.h

and the book.cpp file would be reduced to this:

book1in.cpp

2. Special Functions

2.1 Constructors

Initializing Data

noConstructors.h

Address addr;
addr.putStreet ("21 Pennsylvania Ave.");
addr.putCity ("Washington");
addr.putState ("D.C.");
addr.putZip ("10001");

Author doe;
doe.putName ("Doe, John");
doe.putAddress (addr);

Problems with Field-By-Field Initialization

That leads to a real possibility of our using addr before all the data fields have been initialized.

Constructor Functions

C++ provides a special kind of member function to streamline the initialization process. It’s called a constructor.

Suppose we add a constructor to each of our Address and Author classes.

withConstructors.h

Declaring Variables with Constructors

Then we can create a new author object much more easily:

Address addr ("21 Pennsylvania Ave.",
              "Washington",
              "D.C.", "10001");

Author doe ("Doe, John", addr, 1230157);

or, since the addr variable is probably only there only for the purpose of initializing this author and is probably not used elsewhere, we can do:

Author doe ("Doe, John", 
            Address (
             "21 Pennsylvania Ave.",
             "Washington",
             "D.C.", "10001"),
             1230157);

Implementing Constructors

The implementation of this constructor is pretty straightforward.

Address::Address 
  (std::string theStreet, std::string theCity,
   std::string theState, std::string theZip)
{
  street = theStreet;
  city = theCity;
  state = theState;
  zip = theZip;
}

Initialization Lists

The implementation of the Author constructor has one complication.


Author::Author (std::string theName,
                Address theAddress, long id)
  : identifier (id)
{
  name = theName;
  address = theAddress;
}


identifier = id;

in the function body, because identifier was declared as being const and so cannot be assigned to.

Initialization List Details

Example: Alternate Constructors

Author::Author (std::string theName,
                Address theAddress, long id)
  : identifier (id)
{
  name = theName;
  address = theAddress;
}

or

Author::Author (std::string theName, 
                Address theAddress, long id)
   : name(theName), address(theAddress),
    identifier(id)
{
}

The second constructor might run slightly faster.

2.2 Destructors

The flip-side of initializing new objects is cleaning up when old objects are going away.

A destructor for the class Foo is a function of the form

~Foo();

Destructors are never called explicitly. Instead the compiler generates a call to an object’s destructor for us.

The Compiler Calls a Destructor when…

if (someTest)
  {
   Book b = text361;
   cout << b.getTitle() << endl;
  }

what the compiler would actually generate would be something along the lines of


if (someTest)
  {
   Book b = text361;
   cout << b.getTitle() << endl;
   b.~Book();  // implicitly generated by the compiler
  }


The Compiler Calls a Destructor when…

  Book *bPointer = new Book(text361); // initialized using copy constructor
     ⋮
  cout << bPointer->getTitle() << endl;
  delete bPointer;

what the compiler would actually generate would be something along the lines of


  Book *bPointer = new Book(text361); // initialized using copy constructor
      ⋮
  cout << bPointer->getTitle() << endl;
  bPointer->~Book();
  free(bPointer);  // recover memory at the address in bPointer



Inside a Destructor

The most common uses for destructors is to clean up allocated memory for an object that is about to disappear.

2.3 Operators

Almost every operator in the language can be declared in our own classes. Almost all of the things we use as operators,

are actually “syntactic sugar” for a function whose name if formed by appending the operator itself to the word “operator”.

Operators as Shorthand

For example,

operator+(a, operator*(b, operator-(c)))
testValue = (x <= y);

that is a shorthand for

operator=(testValue, operator<=(x, y);

Declaring Operators

Knowing the shorthand, we can now declare new operators in the same way we declare any new function. E.g.,

Book operator+ (const Book& left, const Book& right);
   and then call it like this: 
Book b = book1 + book2;

but that’s probably not a good idea, because it’s not clear just what it means to add two books together. There are, however, a few operators that would make sense for Book and for almost all ADTs.

Assignment

Chief among these is the assignment operator.

We’ll look more closely at when and how to write our own assignment operators in the next lesson.

I/O

Another, very common set of operators that programmers often write for their own code are the I/O operators << and >>, particularly the output operator <<.

Output Operator Example

Here’s a possible output routine for our Book class.

bookOutput.cpp

Output Operator Example

bookOutput.cpp

cout << book1 << " is better than " 
     << book2 << endl;
     which is treated by the compiler as if we had written: 
(((cout << book1) << " is better than ") 
        << book2) << endl;

so that each output operation, in turn, passes the stream on to the next one in line.

Comparisons

After assignments and I/O, the most commonly programmed operators would be the relational operators, especially == and <.

class Address
{
   ⋮
  bool operator== (const Address&) const;
   ⋮
};

The trickiest thing about providing these operators is making sure we understand just what they should mean for each individual ADT.

Equality via All Data Members Equal

This would be a reasonable implementation:

bool Address::operator== (const Address& right) const
{
  return (street == right.street)
      && (city   == right.city)
      && (state  == right.state)
      && (zip    == right.zip);
}

We could do something similar for Author:

bool Author::operator== (const Author& right) const
{
  return (name       == right.name)
      && (address    == right.address)
      && (identifier == right.identifier);
}

(which, interestingly, makes use of the Address::operator== that we have just defined).

Equality via “keys”

But there’s actually another choice that might be reasonable. If every author has a unique, unchanging identifier, we might be able to just say:

bool Author::operator== (const Author& right) const
{
  return (identifier == right.identifier);
}

on the grounds that two Author objects with the same identifier value must actually be describing the same person, even if they are inconsistent in the other fields.

How Many Relational Ops do we Need?

    using namespace std::rel_ops;

Designing a Good <

In C++, we traditionally provide operator< whenever possible

Again, just what this should mean depends upon the abstraction we are trying to support, but

Example: Comparing Authors

For example, this would be a reasonable pair of comparison operators for Authors:

bool Author::operator== (const Author& right) const
{
  return (identifier == right.identifier);
}

bool Author::operator< (const Author& right) const
{
  return (identifier < right.identifier);
}

Example: Comparing Addresses

Here is a reasonable pair for Address:

AddressRelops.cpp

3. Example: Multiple Implementations of Book

One of the characteristics we expect when working with ADTs is that the implementing data structures and algorithms can be altered without changing the interface.

3.1 Simple Arrays

The authors are stored in an array of fixed size, statically allocated as a part of the Book object.

book1in.h

Adding and Removing

book1in.cpp

3.2 Dynamically Allocated Arrays

A more flexible approach can be obtained by allocating the array of authors on the heap.

In this approach, each book object occupies two distinct blocks of memory, one of them an array allocated on the heap.

book2a.h

In the .h file, the statically sized array authors is replaced by a pointer to an (array of) Author.

Dynamically Allocated Arrays (cont.)

book2a.cpp

The code is still pretty straightforwardly array-based.

3.3 Linked Lists

A still more flexible approach can be obtained by using a linked list of authors. In this case, I have chosen a doubly-linked list (pointers going both forward and backward from each node).

In this approach, each book object occupies many blocks of memory, most of them being linked list nodes allocated on the heap.

book3a.h

The Book itself holds pointers to the first and the last node in the chain.

Linked Lists impl

book3a.cpp

The code is considerably messier - pointer manipulation can be tricky, no matter how many times you do it.

3.4 Standard List

For our final implementation, I present an implementation based upon std::list.

We presume that the data in this approach is arranged pretty much as before, though we don’t really know that for sure because we trust the abstraction provided by the std::list ADT.

book4.h

book4.cpp

The code is quite simple, at least for anyone already familiar with the std::list ADT.

4. Example: Implementing Book::AuthorPosition

Our Book ADT calls for a “helper” ADT, the AuthorPosition, to represent the position of a particular author within the author list for the book.

Iterator Interface Review

To review, our iterator must supply the following operations:

class AuthorPosition {
public:
   AuthorPosition();

   // get data at this position
   Author operator*() const;

   // get a data/function member at this position
   Author* operator->() const;

   // move forward to the position just after this one
   AuthorPosition operator++();

   // Is this the same position as pos?
   bool operator== (const AuthorPosition& pos) const;
   bool operator!= (const AuthorPosition& pos) const;

};

4.1 Simple Arrays

book1in.h

If we implement Book using simple arrays, the implementation of AuthorPosition is trivial.

It takes advantage of the fact that the conventional interface for iterators in C++ is chosen to mimic the behavior of pointers to array elements.

Iterators and Arrays

The only differences lie in how one gets access to the starting and ending positions.

T a[N]; Container c;
get element at position *p p-> *it it->
move forward 1 position ++p ++it
compare positions p1 == p2 it1 == it2
p1!= p2 it1 != it2
position of first element a c.begin()
position just after last element a+N c.end()

Book (Array) Iterator

class Book {
public:
   typedef const Author* AuthorPosition;
     ⋮
};

Book::AuthorPosition Book::begin() const 
{
 return authors; 
}

Book::AuthorPosition Book::end() const 
{
 return authors + numAuthors; 
}

The pointer type already provides the *, ->, ++, ==, and != operations we need.

4.2 Dynamically Allocated Arrays

book2a.h

Everything we have said about iterators over simple arrays applies equally well to dynamic arrays.

So the implementation of AuthorPostion is identical:

class Book {
public:
 typedef const Author* AuthorPosition; 
    ⋮

4.3 Linked Lists

book3a.h

To get the desired behaviors for our iterator ADT, we will need to implement it as a class in its own right. The declaration shown here provides our desired interface.

authorIterator.h

Inside the Book class, we write

class Book { 
public:
   typedef AuthorIterator AuthorPosition; 
     ⋮
 friend class AuthorIterator;
};

which gives AuthorPosition access to all private members of Book.

Implementing the Iterator Ops

For example, we implement the * operator like this:

Author AuthorIterator::operator*() const 
{
  return pos->au; 
} 

returning the data field (only) from the linked list node.

Implementing the Iterator Ops (cont.)

The other particularly interesting operator is ++:

// prefix form ++i; 
const AuthorIterator& AuthorIterator::operator++() 
{
  pos = pos->next;
  return *this; 
}

// postfix form i++; 
AuthorIterator AuthorIterator::operator++(int) 
{
  AuthorIterator oldValue = *this;
  pos = pos->next; return oldValue; 
} 

The main thing that happens here is simple advancing the pos pointer to the next linked list node.

4.4 Standard List

book4.h

If we use a std::list to keep our authors, the implementation of our iterator becomes fairly simple again.

class Book { 
public:
  typedef std::list<Author>::const_iterator AuthorPosition;
  typedef std::list<Author>::const_iterator const_AuthorPosition; 

Implementing the Iterator Ops

Implementing the begin() and end() functions is pretty straightforward.

stdlistIterators.cpp

.