Implementing ADTs in C++ Classes

Steven J Zeil

Last modified: Dec 19, 2017

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
class Book {
public:
  Book (Author)                 // for books with single authors
  Book (Author[], int nAuthors) // for books with multiple authors

  std::string getTitle() const;
  void putTitle(std::string theTitle);

  int getNumberOfAuthors() const;

  std::string getIsBN() const;
  void putISBN(std::string id);

  Publisher getPublisher() const;
  void putPublisher(const Publisher& publ);

  AuthorPosition begin();
  AuthorPosition end();

  void addAuthor (AuthorPosition at, const Author& author);
  void removeAuthor (AuthorPosition at);

private:
  ⋮
};

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.

bookInSeries.h
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
#ifndef BOOK_H
#include "author.h"
#include "publisher.h"


class Book {
public:
  typedef const Author* AuthorPosition;

  Book (Author);                       // for books with single authors
  Book (const Author[], int nAuthors); // for books with multiple authors


  std::string getTitle() const;
  void setTitle(std::string theTitle);

  int getNumberOfAuthors() const;

  std::string getISBN() const;
  void setISBN(std::string id);

  Publisher getPublisher() const;
  void setPublisher(const Publisher& publ);

  AuthorPosition begin() const;
  AuthorPosition end() const;

  void addAuthor (AuthorPosition at, const Author& author);
  void removeAuthor (AuthorPosition at);

private:

  std::string title;
  int numAuthors;
  std::string isbn;
  Publisher publisher;

  static const int MAXAUTHORS = 12;
  Author authors[MAXAUTHORS];

};

#endif

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

book1.cpp
#include "book1.h"

  // for books with single authors
Book::Book (Author a)
{
  numAuthors = 1;
  authors[0] = a;
}

// for books with multiple authors
Book::Book (const Author au[], int nAuthors)
{
  numAuthors = nAuthors;
  for (int i = 0; i < nAuthors; ++i)
    {
      authors[i] = au[i];
    }
}

std::string Book::getTitle() const
{
  return title;
}

void Book::setTitle(std::string theTitle)
{
  title = theTitle;
}

int Book::getNumberOfAuthors() const
{
  return numAuthors;
}

std::string Book::getISBN() const
{
  return isbn;
}

void Book::setISBN(std::string id)
{
  isbn = id;
}

Publisher Book::getPublisher() const
{
  return publisher;
}

void Book::setPublisher(const Publisher& publ)
{
  publisher = publ;
}

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

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


void Book::addAuthor (Book::AuthorPosition at, const Author& author)
{
  int i = numAuthors;
  int atk = at - authors;
  while (i >= atk) 
    {
      authors[i+1] = authors[i];
      i--;
    }
  authors[atk] = author;
  ++numAuthors;
}


void Book::removeAuthor (Book::AuthorPosition at)
{
  int atk = at - authors;
  while (atk + 1 < numAuthors)
    {
      authors[atk] = authors[atk + 1];
      ++atk;
    }
  --numAuthors;
}



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
#ifndef BOOK_H
#include "author.h"
#include "publisher.h"


class Book {
public:
  typedef const Author* AuthorPosition;

  Book (Author);                       // for books with single authors
  Book (const Author[], int nAuthors); // for books with multiple authors


  std::string getTitle() const        { return title; }

  void setTitle(std::string theTitle) {  title = theTitle; }

  int getNumberOfAuthors() const { return numAuthors; }

  std::string getISBN() const  { return isbn; }
  void setISBN(std::string id) { isbn = id; }

  Publisher getPublisher() const { return publisher; }
  void setPublisher(const Publisher& publ) { publisher = publ; }

  AuthorPosition begin() const;
  AuthorPosition end() const;

  void addAuthor (AuthorPosition at, const Author& author);
  void removeAuthor (AuthorPosition at);

private:

  std::string title;
  int numAuthors;
  std::string isbn;
  Publisher publisher;

  static const int MAXAUTHORS = 12;
  Author authors[MAXAUTHORS];

};

#endif

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

book1in.cpp
#include "book1.h"

  // for books with single authors
Book::Book (Author a)
{
  numAuthors = 1;
  authors[0] = a;
}

// for books with multiple authors
Book::Book (const Author au[], int nAuthors)
{
  numAuthors = nAuthors;
  for (int i = 0; i < nAuthors; ++i)
    {
      authors[i] = au[i];
    }
}


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

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


void Book::addAuthor (Book::AuthorPosition at, const Author& author)
{
  int i = numAuthors;
  int atk = at - authors;
  while (i >= atk) 
    {
      authors[i+1] = authors[i];
      i--;
    }
  authors[atk] = author;
  ++numAuthors;
}


void Book::removeAuthor (Book::AuthorPosition at)
{
  int atk = at - authors;
  while (atk + 1 < numAuthors)
    {
      authors[atk] = authors[atk + 1];
      ++atk;
    }
  --numAuthors;
}


2 Special Functions

2.1 Constructors


Initializing Data

noConstructors.h
class Address {
public:
  std::string getStreet() const;
  void putStreet (std::string theStreet);
  
  std::string getCity() const;
  void putCity (std::string theCity);
  
  std::string getState() const;
  void putState (std::string theState);
  
  std::string getZip() const;
  void putZip (std::string theZip);
  
private:
  std::string street;
  std::string city;
  std::string state;
  std::string zip;
};


class Author
{
public:
  std::string getName() const        {return name;}
  void putName (std::string theName) {name = theName;}

  const Address& getAddress() const   {return address;}
  void putAddress (const Address& addr) {address = addr;}

  long getIdentifier() const     {return identifier;}

private:
  std::string name;
  Address address;
  const long identifier;
};
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
class Address {
public:
  Address (std::string theStreet, std::string theCity,
           std::string theState, std::string theZip);

  std::string getStreet() const;
  void putStreet (std::string theStreet);
  
  std::string getCity() const;
  void putCity (std::string theCity);
  
  std::string getState() const;
  void putState (std::string theState);
  
  std::string getZip() const;
  void putZip (std::string theZip);
  
private:
  std::string street;
  std::string city;
  std::string state;
  std::string zip;
};


class Author
{
public:
  Author (std::string theName, Address theAddress, long id);

  std::string getName() const        {return name;}
  void putName (std::string theName) {name = theName;}

  const Address& getAddress() const   {return address;}
  void putAddress (const Address& addr) {address = addr;}

  long getIdentifier() const     {return identifier;}

private:
  std::string name;
  Address address;
  const long identifier;
};

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.

initializationList.cpp
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.


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

implicitDestruction.cpp
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

deletionDestructor.cpp
  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,

the [ ] used in array indexing and the ( ) used in function calls, * but not . or ::,

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

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
ostream& operator<< (ostream& out, const Book& b)
{
  out << b.getISBN() << "\n";
  out << b.getTitle() << "\n";
  for (AuthorPosition current = b.begin(); current != b.end(); ++current)
  {
    Author au = *current;
    out << au << ", ";;
  }
  out << "\n  published by" << b.getPublisher();
  out << endl;
  return out;
}

Output Operator Example

bookOutput.cpp
ostream& operator<< (ostream& out, const Book& b)
{
  out << b.getISBN() << "\n";
  out << b.getTitle() << "\n";
  for (AuthorPosition current = b.begin(); current != b.end(); ++current)
  {
    Author au = *current;
    out << au << ", ";;
  }
  out << "\n  published by" << b.getPublisher();
  out << endl;
  return out;
}
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
bool Address::operator== (const Address& right) const
{
  return (street == right.street)
      && (city   == right.city)
      && (state  == right.state)
      && (zip    == right.zip);
}

bool Address::operator< (const Address& right) const
{
  if (street < right.street)
    return true;
  else if (street == right.street)
    {
     if (city < right.city)
       return true;
     else if (city == right.city)
       {
        if (state < right.state)
          return true;
        else if (state == right.state)
          {
           if (zip < right.zip)
            return true;
          }
       }
    }
  return false;
}

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
#ifndef BOOK_H
#include "author.h"
#include "publisher.h"


class Book {
public:
  typedef const Author* AuthorPosition;

  Book (Author);                       // for books with single authors
  Book (const Author[], int nAuthors); // for books with multiple authors


  std::string getTitle() const        { return title; }

  void setTitle(std::string theTitle) {  title = theTitle; }

  int getNumberOfAuthors() const { return numAuthors; }

  std::string getISBN() const  { return isbn; }
  void setISBN(std::string id) { isbn = id; }

  Publisher getPublisher() const { return publisher; }
  void setPublisher(const Publisher& publ) { publisher = publ; }

  AuthorPosition begin() const;
  AuthorPosition end() const;

  void addAuthor (AuthorPosition at, const Author& author);
  void removeAuthor (AuthorPosition at);

private:

  std::string title;
  int numAuthors;
  std::string isbn;
  Publisher publisher;

  static const int MAXAUTHORS = 12;
  Author authors[MAXAUTHORS];

};

#endif

Adding and Removing

book1in.cpp
#include "book1.h"

  // for books with single authors
Book::Book (Author a)
{
  numAuthors = 1;
  authors[0] = a;
}

// for books with multiple authors
Book::Book (const Author au[], int nAuthors)
{
  numAuthors = nAuthors;
  for (int i = 0; i < nAuthors; ++i)
    {
      authors[i] = au[i];
    }
}


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

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


void Book::addAuthor (Book::AuthorPosition at, const Author& author)
{
  int i = numAuthors;
  int atk = at - authors;
  while (i >= atk) 
    {
      authors[i+1] = authors[i];
      i--;
    }
  authors[atk] = author;
  ++numAuthors;
}


void Book::removeAuthor (Book::AuthorPosition at)
{
  int atk = at - authors;
  while (atk + 1 < numAuthors)
    {
      authors[atk] = authors[atk + 1];
      ++atk;
    }
  --numAuthors;
}


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
#ifndef BOOK_H
#include "author.h"
#include "publisher.h"


class Book {
public:
  typedef const Author* AuthorPosition;

  Book (Author);                       // for books with single authors
  Book (const Author[], int nAuthors); // for books with multiple authors


  std::string getTitle() const        { return title; }

  void setTitle(std::string theTitle) {  title = theTitle; }

  int getNumberOfAuthors() const { return numAuthors; }

  std::string getISBN() const  { return isbn; }
  void setISBN(std::string id) { isbn = id; }

  Publisher getPublisher() const { return publisher; }
  void setPublisher(const Publisher& publ) { publisher = publ; }


  AuthorPosition begin() const;
  AuthorPosition end() const;

  void addAuthor (AuthorPosition at, const Author& author);
  void removeAuthor (AuthorPosition at);

private:

  std::string title;
  int numAuthors;
  std::string isbn;
  Publisher publisher;

  int MAXAUTHORS;
  Author* authors;

};

#endif

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
#include "book2.h"

  // for books with single authors
Book::Book (Author a)
{
  MAXAUTHORS = 4;
  authors = new Author[MAXAUTHORS];
  numAuthors = 1;
  authors[0] = a;
}

// for books with multiple authors
Book::Book (const Author au[], int nAuthors)
{
  MAXAUTHORS = 4;
  authors = new Author[MAXAUTHORS];
  numAuthors = nAuthors;
  for (int i = 0; i < nAuthors; ++i)
    {
      authors[i] = au[i];
    }
}


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

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


void Book::addAuthor (Book::AuthorPosition at, const Author& author)
{
  if (numAuthors >= MAXAUTHORS)
    {
      Author* newAuthors = new Author[2*MAXAUTHORS];
      for (int i = 0; i < MAXAUTHORS; ++i)
	newAuthors[i] = authors[i];
      MAXAUTHORS *= 2;
      delete [] authors;
      authors = newAuthors;
    }
  int i = numAuthors;
  int atk = at - authors;
  while (i > atk) 
    {
      authors[i] = authors[i-1];
      i--;
    }
  authors[atk] = author;
  ++numAuthors;
}


void Book::removeAuthor (Book::AuthorPosition at)
{
  int atk = at - authors;
  while (atk + 1 < numAuthors)
    {
      authors[atk] = authors[atk + 1];
      ++atk;
    }
  --numAuthors;
}

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
#ifndef BOOK_H
#include "author.h"
#include "authoriterator.h"
#include "publisher.h"


class Book {
public:
  typedef AuthorIterator AuthorPosition;

  Book (Author);                       // for books with single authors
  Book (const Author[], int nAuthors); // for books with multiple authors


  std::string getTitle() const        { return title; }

  void setTitle(std::string theTitle) {  title = theTitle; }

  int getNumberOfAuthors() const { return numAuthors; }

  std::string getISBN() const  { return isbn; }
  void setISBN(std::string id) { isbn = id; }

  Publisher getPublisher() const { return publisher; }
  void setPublisher(const Publisher& publ) { publisher = publ; }

  AuthorPosition begin() const;
  AuthorPosition end() const;

  void addAuthor (AuthorPosition at, const Author& author);
  void removeAuthor (AuthorPosition at);

private:

  std::string title;
  int numAuthors;
  std::string isbn;
  Publisher publisher;

  AuthorNode* first;
  AuthorNode* last;

  friend class AuthorIterator;  
};

#endif

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


Linked Lists impl

book3a.cpp
#include "authoriterator.h"
#include "book3.h"

  // for books with single authors
Book::Book (Author a)
{
  numAuthors = 1;
  first = last = new AuthorNode (a, 0, 0);
}

// for books with multiple authors
Book::Book (const Author au[], int nAuthors)
{
  numAuthors = 0;
  first = last = 0;
  for (int i = 0; i < nAuthors; ++i)
    {
      addAuthor(end(), au[i]);
    }
}


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

Book::AuthorPosition Book::end() const
{
  return AuthorPosition(0);
}


void Book::addAuthor (Book::AuthorPosition at, const Author& author)
{
  if (first == 0)
     first = last = new AuthorNode (author, 0, 0);
  else if (at.pos == 0)
    {
      last->next = new AuthorNode (author, last, 0);
      last = last->next;
    }
  else
    {
      AuthorNode* newNode = new AuthorNode(author, at.pos->prev, at.pos);
      at.pos->prev = newNode;
      if (at.pos == first)
	first = newNode;
      else
	newNode->prev->next = newNode;
    }
  ++numAuthors;
}


void Book::removeAuthor (Book::AuthorPosition at)
{
  if (at.pos == first)
    {
      if (first == last)
        first = last = 0;
      else
	{
	  first = first->next;;
	  first->prev = 0;
	}
    }
  else if (at.pos == last)
    {
      last = last->prev;
      last->next = 0;
    }
  else 
    {
      AuthorNode* prv = at.pos->prev;
      AuthorNode* nxt = at.pos->next;
      prv->next = nxt;
      nxt->prev = prv;
    }
  delete at.pos;
  --numAuthors;
}

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

3.4 Standard 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
#ifndef BOOK_H

#include <list>

#include "author.h"
#include "publisher.h"


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

  Book (Author);                       // for books with single authors
  Book (const Author[], int nAuthors); // for books with multiple authors


  std::string getTitle() const        { return title; }

  void setTitle(std::string theTitle) {  title = theTitle; }

  int getNumberOfAuthors() const { return numAuthors; }

  std::string getISBN() const  { return isbn; }
  void setISBN(std::string id) { isbn = id; }

  Publisher getPublisher() const { return publisher; }
  void setPublisher(const Publisher& publ) { publisher = publ; }

  const_AuthorPosition begin() const;
  const_AuthorPosition end() const;

  AuthorPosition begin();
  AuthorPosition end();

  void addAuthor (AuthorPosition at, const Author& author);
  void removeAuthor (AuthorPosition at);

private:

  std::string title;
  int numAuthors;
  std::string isbn;
  Publisher publisher;

  std::list<Author> authors;
};

#endif
book4.cpp
#include "authoriterator.h"
#include "book4.h"

  // for books with single authors
Book::Book (Author a)
{
  numAuthors = 1;
  authors.push_back(a);
}

// for books with multiple authors
Book::Book (const Author au[], int nAuthors)
{
  numAuthors = nAuthors;
  for (int i = 0; i < nAuthors; ++i)
    {
      authors.push_back(au[i]);
    }
}


Book::const_AuthorPosition Book::begin() const
{
  return authors.begin();
}

Book::const_AuthorPosition Book::end() const
{
  return authors.end();
}


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

Book::AuthorPosition Book::end()
{
  return authors.end();
}


void Book::addAuthor (Book::AuthorPosition at, const Author& author)
{
  authors.insert (at, author);
  ++numAuthors;
}


void Book::removeAuthor (Book::AuthorPosition at)
{
  authors.erase (at);
  --numAuthors;
}


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
#ifndef BOOK_H
#include "author.h"
#include "publisher.h"


class Book {
public:
  typedef const Author* AuthorPosition;

  Book (Author);                       // for books with single authors
  Book (const Author[], int nAuthors); // for books with multiple authors


  std::string getTitle() const        { return title; }

  void setTitle(std::string theTitle) {  title = theTitle; }

  int getNumberOfAuthors() const { return numAuthors; }

  std::string getISBN() const  { return isbn; }
  void setISBN(std::string id) { isbn = id; }

  Publisher getPublisher() const { return publisher; }
  void setPublisher(const Publisher& publ) { publisher = publ; }

  AuthorPosition begin() const;
  AuthorPosition end() const;

  void addAuthor (AuthorPosition at, const Author& author);
  void removeAuthor (AuthorPosition at);

private:

  std::string title;
  int numAuthors;
  std::string isbn;
  Publisher publisher;

  static const int MAXAUTHORS = 12;
  Author authors[MAXAUTHORS];

};

#endif

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
#ifndef BOOK_H
#include "author.h"
#include "publisher.h"


class Book {
public:
  typedef const Author* AuthorPosition;

  Book (Author);                       // for books with single authors
  Book (const Author[], int nAuthors); // for books with multiple authors


  std::string getTitle() const        { return title; }

  void setTitle(std::string theTitle) {  title = theTitle; }

  int getNumberOfAuthors() const { return numAuthors; }

  std::string getISBN() const  { return isbn; }
  void setISBN(std::string id) { isbn = id; }

  Publisher getPublisher() const { return publisher; }
  void setPublisher(const Publisher& publ) { publisher = publ; }


  AuthorPosition begin() const;
  AuthorPosition end() const;

  void addAuthor (AuthorPosition at, const Author& author);
  void removeAuthor (AuthorPosition at);

private:

  std::string title;
  int numAuthors;
  std::string isbn;
  Publisher publisher;

  int MAXAUTHORS;
  Author* authors;

};

#endif

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
#ifndef BOOK_H
#include "author.h"
#include "authoriterator.h"
#include "publisher.h"


class Book {
public:
  typedef AuthorIterator AuthorPosition;

  Book (Author);                       // for books with single authors
  Book (const Author[], int nAuthors); // for books with multiple authors


  std::string getTitle() const        { return title; }

  void setTitle(std::string theTitle) {  title = theTitle; }

  int getNumberOfAuthors() const { return numAuthors; }

  std::string getISBN() const  { return isbn; }
  void setISBN(std::string id) { isbn = id; }

  Publisher getPublisher() const { return publisher; }
  void setPublisher(const Publisher& publ) { publisher = publ; }

  AuthorPosition begin() const;
  AuthorPosition end() const;

  void addAuthor (AuthorPosition at, const Author& author);
  void removeAuthor (AuthorPosition at);

private:

  std::string title;
  int numAuthors;
  std::string isbn;
  Publisher publisher;

  AuthorNode* first;
  AuthorNode* last;

  friend class AuthorIterator;  
};

#endif

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
class AuthorIterator {
public:
  AuthorIterator ();

  Author operator*() const;
  const Author* operator->() const;
  const AuthorIterator& operator++(); // prefix form ++i;
  AuthorIterator operator++(int); // postfix form i++;

  bool operator== (const AuthorIterator& ai) const;
  bool operator!= (const AuthorIterator& ai) const; 

private: 
  AuthorNode* pos;
  AuthorIterator (AuthorNode* p)
    : pos(p) 
  {}

  friend class Book; 
};

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
#ifndef BOOK_H

#include <list>

#include "author.h"
#include "publisher.h"


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

  Book (Author);                       // for books with single authors
  Book (const Author[], int nAuthors); // for books with multiple authors


  std::string getTitle() const        { return title; }

  void setTitle(std::string theTitle) {  title = theTitle; }

  int getNumberOfAuthors() const { return numAuthors; }

  std::string getISBN() const  { return isbn; }
  void setISBN(std::string id) { isbn = id; }

  Publisher getPublisher() const { return publisher; }
  void setPublisher(const Publisher& publ) { publisher = publ; }

  const_AuthorPosition begin() const;
  const_AuthorPosition end() const;

  AuthorPosition begin();
  AuthorPosition end();

  void addAuthor (AuthorPosition at, const Author& author);
  void removeAuthor (AuthorPosition at);

private:

  std::string title;
  int numAuthors;
  std::string isbn;
  Publisher publisher;

  std::list<Author> authors;
};

#endif

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
Book::const_AuthorPosition Book::begin() const 
{
  return authors.begin(); 
}

Book::const_AuthorPosition Book::end() const 
{
  return authors.end(); 
}

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

Book::AuthorPosition Book::end() 
{
  return authors.end(); 
}

.