ADT Interfaces in C++

Steven J. Zeil

Last modified: Apr 13, 2024
Contents:

An ADT is an interface to a collection of data.

In C++, the usual mechanism for realizing an ADT is the public part of a class.

In this lesson, we look at how to write interfaces that express the abstract idea that we have about different kinds of data.

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?

Well, to a certain degree, it depends upon whether or not we already have a data type that provides the interface (data and functions) that we want for this new one.

In that case, we can get by just using a typedef to create an “alias” for the existing type. For example, suppose that we were writing a program to look up the zip code for any given city. Obviously, one of the kinds of data we will be manipulating will be “city names”. Now, a city name is pretty much just an ordinary character string, and anything we can do to a character string we could probably also do to a city name. So we might very well decide to take advantage of the existing C++ std::string data type and declare our new one this way:

    typedef std::string CityName;

or, a newer syntax,

    using CityName = std::string;




These basically give us a way to attach a more
convenient name to an existing type.

For example, in building a system to track the output of a publishing company, we might write the class shown here (we’ll talk about where the data and function members come from just a bit later).

    class Book {
    public:
      std::string getTitle() const;
      void setTitle(std::string theTitle);

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

      ⋮
    private:
      ⋮
    };

For example, suppose that our publishing company has a number of books that are grouped into series (e.g., “Cookbooks of the World”, volumes 1-28). This leads to the idea (abstraction) of a “book in series”, that would be identical to a regular Book except for providing two addition data items, a series title and a volume number. We might then write the declaration shown here, which creates a new type named “BookInSeries” that is identical to the existing type Book except that it carries two additional data fields and the functions that provide access to them.

```cpp
class BookInSeries: public Book {
   public:
     std::string getSeriesTitle() const;
     void setSeriesTitle(std::string theSeries);

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

```




Inheritance is a powerful technique that lies at the heart of
object-oriented programming. We won't use much of it in this course,
but it is covered extensively in CS 330.

2 Manipulating an ADT: Attributes and Operations

An ADT is more than just a type name. It also provides a description of what other code can do to manipulate that data.

2.1 Attributes

As you work with ADTs, it becomes very clear that many things we do to data are little more than fetching and storing smaller pieces of data that we think of as components of the ADT.

For example,

We call these kinds of properties the attributes of an ADT.

 

We sometimes show the attributes of an ADT using a UML class diagram, like this:

For the most part, attributes will eventually be implemented as private data members in C++, with public get/set functions to provide access to the data. e.g.,

class Book {  
    string title;  
    Publisher publisher;
    string isbn;
public:
    ⋮
    string getTitle() const;
    void setTitle(const string& newTitle);

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

    string getISBN() const;
    void setISBN(const string& newISBN);
};

But that’s not always the case.

For one thing, the data members might need to be a little different. For example, we might note that many books come from the same publisehr, and decide that we would prefer to keep a pointer to a common publisher object instead of a copy in our book:

class Book {  
    string title;  
    Publisher* publisher;
    string isbn;
public:
    ⋮
    const Publisher& getPublisher() const;
    void setPublisher (const Publisher& publ);
    ⋮
};

This may reduce the memory requirements for out program. It would also mean that, should information about the publisher change, e.g., the publisher’s website URL, we would only need to update a single publisher object rather than trying to find and update every copy of that publisher in every book from that publisher.

That decision to use a pointer might not alter our intended interface and certainly would not change our idea that we are fetching and storing publishers of a book.

Or, we may decide that some of our attributes should be immutable – not subject to change once the Book has been initialized. We might decide, for example, that a book can never change its ISBN, in which case we would provide a “get” but not a “set” function:

class Book {  
    string title;  
    Publisher publisher;
    string isbn;
public:
    ⋮
    string getTitle() const;
    void setTitle(const string& newTitle);

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

    string getISBN() const;  // no setISBN() -- ISBN is immutable
};

Both of these variants would still be described as

 

i.e., a Book has title, publisher, and ISBN attributes.

 

In addition, there may be clever ways to store data that don’t follow the simple “attribute == data member” rule. One of my favorite examples is the idea of a calendar date. We think of dates as having attributes day of the month, month, and year. But it’s rare to see this implemented as

class Date {
    int day;
    int month;
    int year;
public:
      ⋮
    int getYear() const;
    void setYear(int);

    int getMonth() const;
    void setMonth(int);

    int getDay() const;
    void setDay(int);
};

That’s because some of the most common operations we would like to perform on calendar dates are things like “get the day one week before this one”, or “how many days apart are these two dates?” And those sorts of calculations are much easier with a different underlying data structure:

class Date {
    int totalDaysSinceEpoch;
    // Epoch is Jan 1, 1970
public:
      ⋮
    int getYear() const;
    void setYear(int);

    int getMonth() const;
    void setMonth(int);

    int getDay() const;
    void setDay(int);
};

With this, we can answer our “date calculation” queries easily: “get the day one week before this one” is simply totalDaysSinceEpoch - 7, and “how many days apart are these two dates?” is date1.totalDaysSinceEpoch - date2.totalDaysSinceEpoch. Now, the calculation to get/set the year/month/day are kind of horrible, but the three-int version of Date would need to do roughly the same calculations in order to answer the date calculation queries.

The moral of this story:

An attribute is a property that we think of as being fetched from and stored in an ADT, regardless of whether we eventually implement it that way.

2.2 Operations

Not everything we do to an ADT is an attribute. There are also operations, manipulations of the data that do not appear to us as simple data storage.

 

The date calculations are examples of “operations”. In UML, we list operations separately in the bottom third area of a class diagram.

In our Book example, we might consider another part of our idea of what makes something book-like:

Now, we could treat that as just another attribute:

 
class Book {  
    string title;  
    Publisher publisher;
    string isbn;
    authors: Author[25];
public:
    ⋮
    string getTitle() const;
    void setTitle(const string& newTitle);

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

    string getISBN() const;
    void setISBN(const string& newISBN);

    const Author* getAuthors() const;
    void setAuthors(Author[] authorList);
};

But this feels clunky. Just imagining writing code to, say, remove an author or change one author’s address if you needed to retrieve the entire array first, find the author you wanted to change, make the change, and then put the whole modified array into place.

It makes much more sense to instead envision operations that let us work with authors one at a time:

 

Now, you might object that this no longer captures the abstract idea that

But that just shows that “has a” does not always mean “attribute”. In fact, UML has a way of saying that one thing is part of another without representing them as attributes:

 

The diamond-headed arrow is called the “aggregation” arrow, and can be read as “is part of”, i.e., “an Author is part of a Book”. The "*“ markers stand for ”many“ or ”an arbitrary number", because a book can have multiple authors, but many authors have written more than one book.

2.3 Example: the Publishing World

In the prior lesson, we used the following to illustrate our data abstraction for things related to books.

 

As we move forward into considering an ADT, we would shift our focus away from the individual objects and more towards the classes:

 

3 Realizing the Interface in C++

Eventually, a C++ programmer must sit down to capture the abstract ideas of attributes and operations in a C++ class. At this stage, we are really only worrying about the public interface of a class. Generally this involves adding public member functions and, rarely, public data members.

Of course, C++ being what it is, there’s lots to think about even when only working on the interface.

3.1 Basic Attributes & Operations

As a first pass, we can translate the attributes and operations into C++.

For example,

 
class Address {
public:
  std::string getStreet() const;
  void setStreet (std::string theStreet);

  std::string getCity() const;
  void setCity (std::string theCity);

  std::string getState() const;
  void setState (std::string theState);

  std::string getZip() const;
  void setZip (std::string theZip);

private:
  ⋮
};

Remember, this lesson is about ADT interfaces. We will worry about the function bodies and data members when we look at ADT implementation;

 
class Author
{
public:

  std::string getName() const;
  void setName (std::string theName);

  const Address& getAddress() const;
  void setAddress (const Address& addr);

  int numberOfBooks() const;
  Book& getBook(int i);

  void addBook (Book& b);
  void removeBook (Book& b);

private:
  ⋮
};

4 Constructors

Most ADTs need a way to initialize their attributes. Of course, we could do it one data field at a time:

Address addr;
addr.setStreet ("21 Pennsylvania Ave.");
addr.setCity ("Washington");
addr.setState ("D.C.");
addr.setZip ("10001");

Author doe;
doe.setName ("Doe, John");
doe.setAddress (addr);

Now this isn’t really all that great.

That, in turn, leads to a real possibility of our using addr or doe before all the data fields have been initialized, with potentially disastrous results.

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

Constructors are unusual in that their name must be the same as their “return type”, the name of the class being initialized. A constructor is called when we define a new variable, and any parameters supplied in the definition are passed as parameters to the constructor.

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

class Address {
public:
  Address (std::string theStreet,
           std::string theCity,
           std::string theState,
           std::string theZip);

  std::string getStreet() const;
  void setStreet (std::string theStreet);

  ⋮
};
class Author
{
public:
  Author (std::string theName, const Address& theAddress);

  std::string getName() const;
  void setName (std::string theName);

   ⋮
};

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

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

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

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 (
               "1600 Pennsylvania Ave.",
               "Washington",
               "D.C.", "10001"),
            1230157);

It’s good to remember that, although constructors play a special role in defining new variables, e.g.

Author doe (...);

they are still functions and can be used, like any function, to return a value inside a more complicated expression:

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

4.1 Constructing Books with Multiple Authors

Our Book class will need a constructor as well, but this one is slightly complicated by the fact that books can have multiple authors. How can we pass an arbitrary number of authors to a constructor (or to any function, for that matter)?

class Book {
public:
   Book(const std::string& title, 
              const std::string& isbn, 
              const Publisher& publisher,
              Author* authors = nullptr,   ➀ 
              int numAuthors = 0);         ➁

      ⋮
};

For now, we’ll do it by passing an array of Authors (), together with an integer indicating how many items are in the array. (In a later lesson, we will see much cleaner ways to handle this problem.)

With that constructor in place, we could initialize books by first building an appropriate author array, then declaring our new Book object. For example, given these authors:

Author weiss (
    "Weiss, Mark",
    Address("21 Nowhere Dr.", "Podunk", "NY", "01010")
    );
Author doe (
    "Doe, John",
    Address("212 Baker St.", "Peoria", "IL", "12345")
    );
Author smith (
    "Smith, Jane",
    Address("47 Scenic Ct.", "Oahu", "HA", "54321")
    );

we can create some books like this:

Publisher prenticeHall = ...;
Author textAuthors[] = {weiss};
Book text361 ("Data Structures and Algorithms in C++", "013284737X",
               macmillan, 
               textAuthors, 1);

Author recipeAuthors[] = {doe, smith};
Book recipes ("Cooking with Gas", "0-124-46821", prenticeHall, 
              recipeAuthors, 2);

Taking advantage of the default parameters, we could write

Book unknownText ("Much Ado About Nothing", "123456789", prenticeHall);

In cases where we really do have multiple authors (e.g., recipes), that’s probably as simple as it’s going to get. It’s a bit awkward for books that only have a single author, however. But, just as with any other functions in C++, nothing limits us to having a single constructor function as long as the formal parameter list for each constructor is different. Since single authorship is likely to be a common case, it might be convenient to declare another Book constructor to serve that special case.

class Book {
public:
   Book(const std::string& title, 
              const std::string& isbn, 
              const Publisher& publisher,
              Author* authors = nullptr,
              int numAuthors = 0);

   Book(const std::string& title, 
              const std::string& isbn, 
              const Publisher& publisher,
              Author& author1);

      ⋮
};

We could then use either constructor, as appropriate, when creating Books.

Book text361 ("Data Structures and Algorithms in C++", "013284737X",
               macmillan, 
               weiss);

Author recipeAuthors[] = {doe, smith};
Book recipes ("Cooking with Gas", "0-124-46821", prenticeHall, 
              recipeAuthors, 2);

4.2 The Default Constructor

The default constructor is a constructor that takes no arguments. This is the constructor you are calling when you declare an object with no explicit initialization parameters. E.g.,

std::string s;

A default constructor might be declared to take no parameters at all, like this:

class Author
{
public:
  Author();

  Author (std::string theName, const Address& theAddress);

    ⋮

Or by supplying a default value for every parameter in an existing constructor:

class Address {
public:

  Address (std::string theStreet = std::string("unknown"),
           std::string theCity = std::string(),
           std::string theState = std::string(),
           std::string theZip = std::string());

    ⋮

Either way, we can call it with no parameters.


Why is this called a “default” constructor? There’s actually nothing particularly special about it. It’s just an ordinary constructor, but it is used in a special way.

Whenever we create an array of elements, the compiler implicitly calls the default constructor to initialize each element of the array.

For example, if we declared:

std::string words[5000];

then each of the 5000 elements of this array will be initialized using the default constructor for string.

In fact, if we don’t have a default constructor for our class, then we can’t create arrays containing that type of data. Attempting to do so will result in a compilation error.

Because arrays are so common, it is rare that we would actually want a class with no default constructor.

4.2.1 Compiler-Generated Constructors

It’s hard to imagine a situation in which we create a class and would deliberately prefer that, when we declare a variable of that class type, its data members should remain uninitialized.

That’s just poor programming practice.

The designers of the C++ language felt the same way. So the C++ compiler tries to be helpful:

If we create no constructors at all for a class, the compiler generates a default constructor for us.

Why does the compiler generate, specifically, a default constructor? Well, any other choice would mean that the compiler would have to figure out what data we needed to supply to initialize each data member, and what we wanted to do with that data to carry out the initialization. That’s well beyond the intelligence of any compiler, so it opts for the simple case of generating a contructor that takes no parameters at all.

That automatically generated default constructor works by initializing every data member in our class with its own default constructor. In many cases (e.g., strings), this does something entirely reasonable for our purposes.

Be careful, though: the “default constructors” for the primitive types such as int, double and pointers work by doing nothing at all — leaving us with an uninitialized value containing whatever bits happened to have been left at that particular memory address by earlier calculations/programs.

4.3 The Copy Constructor

Another specialized constructor, the copy constructor is a constructor that takes a (const reference to a) value of the same type its only parameter. For example:

Book (const Book& b);

4.3.1 Where are Copy Constructors Used?

The copy constructor gets used in 5 situations:

  1. When you declare a new object as a copy of an old one:

    Book book2 (book1);
    

    or

    Book book2 = book1;
    
  2. When a function call passes a parameter “by copy” (i.e., the formal parameter does not have a &):

    void foo (Book b, int k);
      ⋮
    
    Book text361 (...);
    foo (text361, 0);   // foo actually gets a copy of text361
    
  3. When a function returns an object:

    Book foo (int k);
    {
      Book b;
      ⋮
      return b; // a copy of b is placed in the caller's memory area
    }
    
  4. When data members are initialized in a constructor’s initialization list from a single parameter of the same type:

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

    We’ll cover initialization lists in the next lesson.

  5. When an object is a data member of another class for which the compiler has generated its own copy constructor.

4.3.2 Compiler-Generated Copy Constructors

As you can see from that list, the copy constructor gets used a lot. It would be very awkward to work with a class that did not provide a copy constructor.

So, again, the compiler tries to be helpful.

If we do not create a copy constructor for a class, the compiler generates one for us.

5 Destructors

The flip-side of initializing new objects is cleaning up when old objects are going away. Just as C++ provides special functions, constructors, for handling initialization, it also provides special functions, destructors, for handling clean-up.

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

~Foo();

You should never call a destructors explicitly.

Instead the compiler generates a call to an object’s destructor when

Now, we have not declared or implemented a destructor for any of our classes, yet. That might be OK.

If you don’t provide a destructor for a class, the compiler generates one for you automatically.

The automatically generated destructor simply invokes the destructors for any data member objects. We will later discuss the circumstances under which we need to provide our own destructors.

6 Operators

One of the truly delightful, or demonic, depending on your point of view, aspects of C++ is that almost every operator in the language can be declared in our own classes. In most programming languages, we can write things like “ x+y ” or “ x<y ” only if x and y are integers, floating point numbers, or some other pre-defined type in the language. In C++, we can add these operators to any class we design, if we feel that they are appropriate.

The basic idea is really very simple.

Almost of the things we use as operators, including

+ - * / | & < > <= >= = == != ++ -- -> += -= *= /=

can be overloaded.

This is also true of some things you probably don’t consider as operators (the [ ] used in array indexing and the ( ) used in function calls). There are only a few things that look like operators but cannot be overloaded: ‘.’ and ‘::’.

All of those operator symbols are actually “syntactic sugar” for a function whose name is formed by appending the operator itself to the word “operator” .


For example, if you write a + b*(-c), that’s actually just a shorthand for

operator+(a, operator*(b, operator-(c)))

and if you write

testValue = (x <= y);

that’s actually a shorthand for

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

Now, this shorthand is so much easier to deal with that there’s seldom any good reason to actually write out the long form of these calls. But knowing the shorthand means that we can now declare new operators in the same way we declare new functions.

 

6.1 Example: Date Operations

Earlier, for example, we suggested that a calendar date might have some operations like this:

We could declare those in C++ like this:

class Date {
 private:
   int daysSinceEpoch;  
 public:
   Date ();
   Date (int day, int month, int year);
   
   int getDay() const;  
   void setDay (int day);
   
   int getMonth() const;
   void setMonth(int month);
   
   int getYear() const;
   void setYear();
   
   Date dateSomeDaysAway(int numDaysAway) const;
   int numberOfDaysApart(const Date& from) const;
};

This would let us write application code like:

Date rescheduleWeeklyMeeting(const Date& meeting)
{
    Date followUp = meeting.dateSomeDaysAway(7);
    return followUp;
}

or

void warnIfBookIsOverdue (const Date& checkoutDate, const Date& today)
{
    if (today.numberOfDaysApart(checkoutDate) > 21)
    {
        cout << "This book is overdue." << endl;
    }
}

The function bodies for the two operations given as

Date Date::dateSomeDaysAway(int numDaysAway) const
{
    Date result;
    result.daysSinceEpoch = daysSinceEpoch + numDaysAway;
    return result;
}
   
int Date::numberOfDaysApart(const Date& from) const
{
    return daysSinceEpoch - from.daysSinceEpoch;
}

Now, the names of the two operations are a bit awkward, particularly the first one. Perhaps you could think of better ones. But I honestly didn’t worry about it at the time because I knew that I was eventually going to replace these by operators.

 

All that we have done here is to change the names of the two functions, not their behavior. And that’s all that would change in the C++ class declaration:

class Date {
 private:
   int daysSinceEpoch;  
 public:
   Date ();
   Date (int day, int month, int year);
   
   int getDay() const;  
   void setDay (int day);
   
   int getMonth() const;
   void setMonth(int month);
   
   int getYear() const;
   void setYear();
   
   Date operator+(int numDaysAway) const;
   int operator-(const Date& from) const;
};

I think, however, that this makes for much nicer application code:

Date rescheduleWeeklyMeeting(const Date& meeting)
{
    Date followUp = meeting + 7;
    return followUp;
}

or

void warnIfBookIsOverdue (const Date& checkoutDate, const Date& today)
{
    if (today - checkoutDate > 21)
    {
        cout << "This book is overdue." << endl;
    }
}

For the eventual implementation, again, all that changes are the function names:

Date Date::operator+(int numDaysAway) const
{
    Date result;
    result.daysSinceEpoch = daysSinceEpoch + numDaysAway;
    return result;
}
   
int Date::operator-(const Date& from) const
{
    return daysSinceEpoch - from.daysSinceEpoch;
}

Operator functions seem to cause some students a great deal of anxiety, but they really aren’t all that complicated.

When we declare and implement them, it’s just a change to a function name that starts with “operator”.

It is only when we use them that their special nature comes out, as they provide us with a shortcut to writing out the full function name.


Not every class make sense with operators like “+” or “-”. I have no idea, for example, what it would even mean to add two Books together or to subtract one Book from another.

There are, however, a few operators that would make sense for Book and for almost all ADTs.

6.2 Assignment

One operator that makes sense for almost all classes is the assigment operator.

Chief among these is the assignment operator. When we write book1 = book2, that’s shorthand for book1.operator=(book2).

Assignment is used so pervasively that a class without assignment would be severely limited (although sometimes designers may want that limitation). Consequently,

If a class does not provide an explicit assignment operator, the compiler will attempt to generate one. The compiler-generated version will simply assign each data member in turn.

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

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

Indeed, I would argue that you should always provide an output operator for every class you write, even if you don’t expect to use it in your final application.

My reason for this is quite practical. Sooner or later, you’re going to discover that your program isn’t working right. So how are you going to debug it? The most common thing to do is to add debugging output at all the spots where you think things might be going wrong. So you come to some statement:

  doSomethingTo(book1);

and you realize that it might be really useful to know just what the value of that book was before and after the call:

  cerr << "Before doSomethingTo: " << book1 << endl;
  doSomethingTo(book1);
  cerr << "After doSomethingTo: " << book1 << endl;

Now, if you have already written that operator<< function for your Book class, you can proceed immediately. If you haven’t written it already, do you really want to be writing and debugging that new function now, when you are already dealing with a different bug?

We can add output to some of our existing classes:

class Address {
public:

  Address (std::string theStreet = std::string("unknown"),
           std::string theCity = std::string(),
           std::string theState = std::string(),
           std::string theZip = std::string());

  std::string getStreet() const;
  void setStreet (std::string theStreet);

  std::string getCity() const;
  void setCity (std::string theCity);

  std::string getState() const;
  void setState (std::string theState);

  std::string getZip() const;
  void setZip (std::string theZip);

private:
  ⋮
};


std::ostream& operator<< (std::ostream& out, const Address& addr);

The output operator must be declared outside of the class. That’s because, function members of a class are always called with an object of that class type on the left. For example

Address addr;
addr.setState("VA");

For the operator shorthand, that translates to the object being to the left of the operator. E.g., “address1 = address2” is equivalent to “address1.operator=(address2)”.

But look at how we use an output operator:

cout << addr;

The thing on the left is not the ADT we are trying to write – it’s a C++ std::ostream. So operator<< could only be a member function of class std::ostream. And that’s no good for our purposes. So if we want to support output of Address or Author or any of our other classes, we need to do it as an ordinary, non-member function.

Another unusual point: notice the return value on the operator<< declaration. Each implementation of operator<< is supposed to return the output stream to which we are writing. It’s the fact that << is an expression that returns the stream we are writing to that let’s us write “chains” of output expressions like:

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;

Look at the second <<.

So, the left-hand parameter needs to be the output stream that we are writing all of this output to.

By convention, then, every implementation of operator<< returns the stream that it has just written into, so that a chain of << calls will, each in turn, pass the stream on to the next one in line.

6.4 Comparisons

After assignments and I/O, the most commonly programmed operators would be the relational operators, especially == and <. The compiler never generates these implicitly, so if we want them, we have to supply them.

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. For example, if I were to write

if (address1 == address2)

what would I expect to be true of two Addresses that passed this test? Probably that the two addresses would have the same street, city, state, and zip - in other words, that all the data fields should themselves be equal. In that case, 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);
 }

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

In C++, we traditionally provide operator< whenever possible, and many code libraries assume that this operator is available.

Again, just what this should mean depends upon the uses we intend to make of our ADT, but there are a few hard and fast rules:

We should always design operator< so that

7 Other Interface Details

7.1 Const Correctness

In C++, we use the keyword const to declare constants. E.g.,

const double PI = 3.14159;

But it also has two other important uses:


These last two uses are important for a number of reasons


A class is const-correct if

Our current version of Author gives some good examples of this process:

class Author
{
public:
  Author();

  Author (std::string theName, const Address& theAddress);

  std::string getName() const;
  void setName (std::string theName);

  const Address& getAddress() const;
  void setAddress (const Address& addr);

  int numberOfBooks() const;
  std::string& getBook(int i);
  const std::string& getBook(int i) const;

  void addBook (Book& b);
  void removeBook (Book& b);

  bool operator== (const Author& right) const;
  bool operator< (const Author& right) const;


private:
  ⋮
};
 

Similarly, when we use == or < to compare one author to another, we would expect that such a comparison would not affect the value of the author to the left of the operator, so we mark these functions as const also.

7.1.1 Const Member Function Pairs

It’s worth highlighting one peculiarity that const correctness introduces to ADT interfaces. Look at these member functions:

class Author
{
public:
    ⋮
  std::string& getBook(int i);
  const std::string& getBook(int i) const;
    ⋮
};

The first of these will be applied to Authors that are not const. The second will be applied to Author objects that are const. For example,

void transferFirstBook (const Author& a, Author& b)
{
    string title = a.getBook[0];   // uses 2nd, const versions
    b.getBook(0) = title;          // uses 1st, non-const version
    a.getBook(0) = "out of print"; // Compilation error!
}

The first version returns a reference (address) of the author’s book and so can actually be used to change the book inside the Author object.

The second version returns a const reference, which cannot be be changed. And that is entirely consistent with the fact that the book a is itself marked as const, which is a promise by transferFirstBook that it will not do anything that could change the value of a.

This use of paired functions differing only in the const-labels on the function, its parameters, and its return type, is a very common pattern in C++.

7.2 Inline Functions

Many of the member functions we have given as examples are simple enough that we might consider an alternate approach, declaring them as inline functions.

When we define a function, it is usually compiled into a self-contained unit of code. For example, the function

int foo(int a, int b)
{
  return a+b-1;
}

would compile into a block of code equivalent to

stack[1] = stack[3] + stack[2] - 1; 
jump to address in stack[0]

where the “stack” is the runtime stack a.k.a. the activation stack
used to track function calls at the system level, stack[0] is the top value on the stack, stack[1] the value just under that one, and so on. A function call like

  x = foo(y,z+1);

would be compiled into a code sequence along the lines of

push y onto the runtime stack; 
evaluate z+1; 
push the result onto the runtime stack
push (space for the return value) onto the runtime stack 
save all CPU registers
push address RET onto the runtime stack 
jump to start of foo's body 
RET: x = stack[1] 
pop runtime stack 4 times 
restore all CPU registers

As you can see, there’s a fair amount of overhead involved in passing parameters and return address information to a function when making a call. The amount of time spent on this overhead is not really all that large. If the function body contains several statements in any kind of loop, then the overhead is probably a negligable fraction of the total time spent on the call. But if the function body is only a line or two and does not involve a significant amount of computation, then the time spent on that overhead can be, by comparison, quite large.

Many ADTs have member functions that are only one or two lines long, and often trivial lines at that. In our Book class, for example, we might feel that all of our get... and set... functions meet that description.

For these functions, the overhead associated with each call may exceed the time required to do the function body itself. Furthermore, because these functions are often the primary means of accessing the ADT’s contents, sometimes these functions get called thousands of times or more inside the application’s loops, causing the total amount of time spent on the overhead of making the function call to become significant.

For these kinds of trivial functions, C++ offers the option of declaring them as inline.

An inline function can be written one of two ways, both of which are illustrated here:

class Address {
public:

  Address (std::string theStreet = std::string("unknown"),
           std::string theCity = std::string(),
           std::string theState = std::string(),
           std::string theZip = std::string());

  std::string getStreet() const {return street;}
  void setStreet (std::string theStreet) {street = theStreet;}

  std::string getCity() const {return city;}
  void setCity (std::string theCity) {city = theCity;}

  std::string getState() const {return state;}
  void setState (std::string theState) {state = theState;}

  std::string getZip() const {return zipcode;}
  void setZip (std::string theZip) {zipcode = theZip;}

  bool operator== (const Address& right) const;
  bool operator< (const Address& right) const;

private:
  std::string street;
  std::string city;
  std::string state;
  std::string zipcode;
};

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


std::ostream& operator<< (std::ostream& out, const Address& addr);
  1. It can be written inside the class declaration.
  2. We can place the reserved word inline in front of the function definition written in its usual place outside the class declaration.

Either way, we then remove the function body from the .cpp file.

When we make a call to an inline function, the compiler simply replaces the call by a compiled copy of the function body (with some appropriate renaming of variables to avoid conflicts).

So, if we have

inline int foo(int a, int b)
{
  return a+b-1;
}

and we later make a call

  x = foo(y,z+1);

This would be compiled into a code sequence along the lines of

evaluate z+1, storing result in tempB 
evaluate y + tempB - 1, storing result in x

Most of the overhead of making a function call has been eliminated.

Inline functions can reduce the run time of a program by removing unnecessary function calls, but, used unwisely, may also cause the size of the program to explode. Consequently, they should be used only by frequently-called functions with bodies that take only 1 or 2 lines of code. For larger functions, the times savings would be negligible (as a fraction of the total time) while the memory penalty is more severe, and for infrequently used functions, who cares?

Inlining is only a recommendation from the programmer to the compiler. The compiler may ignore an inline declaration and continue treating it as a conventional function if it prefers. In particular, inlining of functions with recursive calls is impossible, as is inlining of most virtual function calls (don’t worry if you don’t know what those are). Many compilers will refuse to inline any function whose body contains a loop. Others may have their own peculiar limitations.


1: : This is the default method of parameter passing in C++. It’s what you get when your formal parameter types do not have an &.