Programming: Principles and Practice Using C++ (2014)

Part I: The Basics

9. Technicalities: Classes, etc.

“Remember, things take time.”

—Piet Hein

In this chapter, we keep our focus on our main tool for programming: the C++ programming language. We present language technicalities, mostly related to user-defined types, that is, to classes and enumerations. Much of the presentation of language features takes the form of the gradual improvement of a Date type. That way, we also get a chance to demonstrate some useful class design techniques.


9.1 User-defined types

9.2 Classes and members

9.3 Interface and implementation

9.4 Evolving a class

9.4.1 struct and functions

9.4.2 Member functions and constructors

9.4.3 Keep details private

9.4.4 Defining member functions

9.4.5 Referring to the current object

9.4.6 Reporting errors

9.5 Enumerations

9.5.1 “Plain” enumerations

9.6 Operator overloading

9.7 Class interfaces

9.7.1 Argument types

9.7.2 Copying

9.7.3 Default constructors

9.7.4 const member functions

9.7.5 Members and “helper functions”

9.8 The Date class


9.1 User-defined types

Image

The C++ language provides you with some built-in types, such as char, int, and double (§A.8). A type is called built-in if the compiler knows how to represent objects of the type and which operations can be done on it (such as + and *) without being told by declarations supplied by a programmer in source code.

Image

Types that are not built-in are called user-defined types (UDTs). They can be standard library types — available to all C++ programmers as part of every ISO standard C++ implementation — such as string, vector, and ostream (Chapter 10), or types that we build for ourselves, such asToken and Token_stream (§6.5 and §6.6). As soon as we get the necessary technicalities under our belt, we’ll build graphics types such as Shape, Line, and Text (Chapter 13). The standard library types are as much a part of the language as the built-in types, but we still consider them user-defined because they are built from the same primitives and with the same techniques as the types we built ourselves; the standard library builders have no special privileges or facilities that you don’t have. Like the built-in types, most user-defined types provide operations. For example,vector has [ ] and size() (§4.6.1, §B.4.8), ostream has <<, Token_stream has get() (§6.8), and Shape has add(Point) and set_color() (§14.2).

Image

Why do we build types? The compiler does not know all the types we might like to use in our programs. It couldn’t, because there are far too many useful types — no language designer or compiler implementer could know them all. We invent new ones every day. Why? What are types good for? Types are good for directly representing ideas in code. When we write code, the ideal is to represent our ideas directly in our code so that we, our colleagues, and the compiler can understand what we wrote. When we want to do integer arithmetic, int is a great help; when we want to manipulate text, string is a great help; when we want to manipulate calculator input, Token and Token_stream are a great help. The help comes in two forms:

• Representation: A type “knows” how to represent the data needed in an object.

• Operations: A type “knows” what operations can be applied to objects.

Many ideas follow this pattern: “something” has data to represent its current value — sometimes called the current state — and a set of operations that can be applied. Think of a computer file, a web page, a toaster, a music player, a coffee cup, a car engine, a cell phone, a telephone directory; all can be characterized by some data and all have a more or less fixed set of standard operations that you can perform. In each case, the result of the operation depends on the data — the “current state” — of an object.

So, we want to represent such an “idea” or “concept” in code as a data structure plus a set of functions. The question is: “Exactly how?” This chapter presents the technicalities of the basic ways of doing that in C++.

Image

C++ provides two kinds of user-defined types: classes and enumerations. The class is by far the most general and important, so we first focus on classes. A class directly represents a concept in a program. A class is a (user-defined) type that specifies how objects of its type are represented, how those objects can be created, how they are used, and how they can be destroyed (see §17.5). If you think of something as a separate entity, it is likely that you should define a class to represent that “thing” in your program. Examples are vector, matrix, input stream, string, FFT (fast Fourier transform), valve controller, robot arm, device driver, picture on screen, dialog box, graph, window, temperature reading, and clock.

In C++ (as in most modern languages), a class is the key building block for large programs — and very useful for small ones as well, as we saw for our calculator (Chapters 6 and 7).

9.2 Classes and members

Image

A class is a user-defined type. It is composed of built-in types, other user-defined types, and functions. The parts used to define the class are called members. A class has zero or more members. For example:

class X {
public:
          int m;                                                                      // data member
          int mf(int v) { int old = m; m=v; return old; }   // function member
};

Members can be of various types. Most are either data members, which define the representation of an object of the class, or function members, which provide operations on such objects. We access members using the object.member notation. For example:

X var;                              // var is a variable of type X
var.m = 7;                       // assign to var’s data member m
int x = var.mf(9);            // call var’s member function mf()

You can read var.m as var’s m. Most people pronounce it “var dot m” or “var’s m.” The type of a member determines what operations we can do on it. We can read and write an int member, call a function member, etc.

A member function, such as X’s mf(), does not need to use the var.m notation. It can use the plain member name (m in this example). Within a member function, a member name refers to the member of that name in the object for which the member function was called. Thus, in the callvar.mf(9), the m in the definition of mf() refers to var.m.

9.3 Interface and implementation

Image

Usually, we think of a class as having an interface plus an implementation. The interface is the part of the class’s declaration that its users access directly. The implementation is that part of the class’s declaration that its users access only indirectly through the interface. The public interface is identified by the label public: and the implementation by the label private:. You can think of a class declaration like this:

class X {       // this class’s name is X
public:
          // public members:
          //      – the interface to users (accessible by all)
          // functions
          // types
          // data (often best kept private)
private:
          // private members:
          //      – the implementation details (used by members of this class only)
          // functions
          // types
          // data
};

Class members are private by default; that is,

class X {
          int mf(int);
          // . . .
};

means

class X {
private:
          int mf(int);
          // . . .
};

so that

X x;                       // variable x of type X
int y = x.mf();      // error: mf is private (i.e., inaccessible)

A user cannot directly refer to a private member. Instead, we have to go through a public function that can use it. For example:

class X {
          int m;
          int mf(int);
public:
          int f(int i) { m=i; return mf(i); }
};

X x;
int y = x.f(2);

We use private and public to represent the important distinction between an interface (the user’s view of the class) and implementation details (the implementer’s view of the class). We explain that and give lots of examples as we go along. Here we’ll just mention that for something that’s just data, this distinction doesn’t make sense. So, there is a useful simplified notation for a class that has no private implementation details. A struct is a class where members are public by default:

struct X {
          int m;
          // . . .
};

means

class X {
public:
          int m;
          // . . .
};

structs are primarily used for data structures where the members can take any value; that is, we can’t define any meaningful invariant (§9.4.3).

9.4 Evolving a class

Let’s illustrate the language facilities supporting classes and the basic techniques for using them by showing how — and why — we might evolve a simple data structure into a class with private implementation details and supporting operations. We use the apparently trivial problem of how to represent a date (such as August 14, 1954) in a program. The need for dates in many programs is obvious (commercial transactions, weather data, calendar programs, work records, inventory management, etc.). The only question is how we might represent them.

9.4.1 struct and functions

How would we represent a date? When asked, most people answer, “Well, how about the year, the month, and the day of the month?” That’s not the only answer and not always the best answer, but it’s good enough for our uses, so that’s what we’ll do. Our first attempt is a simple struct:

// simple Date (too simple?)
struct Date {
          int y;       // year
          int m;     // month in year
          int d;      // day of month
};

Date today;    // a Date variable (a named object)

A Date object, such as today, will simply be three ints:

Image

There is no “magic” relying on hidden data structures anywhere related to a Date — and that will be the case for every version of Date in this chapter.

So, we now have Dates; what can we do with them? We can do everything in the sense that we can access the members of today (and any other Date) and read and write them as we like. The snag is that nothing is really convenient. Just about anything that we want to do with a Date has to be written in terms of reads and writes of those members. For example:

// set today to December 24, 2005
today.y = 2005;
today.m = 24;
today.d = 12;

This is tedious and error-prone. Did you spot the error? Everything that’s tedious is error-prone! For example, does this make sense?

Date x;
x.y = –3;
x.m = 13;
x.d = 32;

Probably not, and nobody would write that — or would they? How about

Date y;
y.y = 2000;
y.m = 2;
y.d = 29;

Was year 2000 a leap year? Are you sure?

What we do then is to provide some helper functions to do the most common operations for us. That way, we don’t have to repeat the same code over and over again and we won’t make, find, and fix the same mistakes over and over again. For just about every type, initialization and assignment are among the most common operations. For Date, increasing the value of the Date is another common operation, so we write

// helper functions:

void init_day(Date& dd, int y, int m, int d)
{
          // check that (y,m,d) is a valid date
          // if it is, use it to initialize dd
}

void add_day(Date& dd, int n)
{
          // increase dd by n days
}

We can now try to use Date:

void f()
{
          Date today;
          init_day(today, 12, 24, 2005);     // oops! (no day 2005 in year 12)
          add_day(today,1);
}

Image

First we note the usefulness of such “operations” — here implemented as helper functions. Checking that a date is valid is sufficiently difficult and tedious that if we didn’t write a checking function once and for all, we’d skip the check occasionally and get buggy programs. Whenever we define a type, we want some operations for it. Exactly how many operations we want and of which kind will vary. Exactly how we provide them (as functions, member functions, or operators) will also vary, but whenever we decide to provide a type, we ask ourselves, “Which operations would we like for this type?”

9.4.2 Member functions and constructors

We provided an initialization function for Dates, one that provided an important check on the validity of Dates. However, checking functions are of little use if we fail to use them. For example, assume that we have defined the output operator << for a Date (§9.8):

void f()
{
          Date today;
          // . . .
          cout << today << '\n';               // use today
          // . . .
          init_day(today,2008,3,30);
          // . . .
          Date tomorrow;
          tomorrow.y = today.y;
          tomorrow.m = today.m;
          tomorrow.d = today.d+1;        // add 1 to today
          cout << tomorrow << '\n';       // use tomorrow
}

Here, we “forgot” to immediately initialize today and “someone” used it before we got around to calling init_day(). “Someone else” decided that it was a waste of time to call add_day() — or maybe hadn’t heard of it — and constructed tomorrow by hand. As it happens, this is bad code — very bad code. Sometimes, probably most of the time, it works, but small changes lead to serious errors. For example, writing out an uninitialized Date will produce garbage output, and incrementing a day by simply adding 1 to its member d is a time bomb: when today is the last day of the month, the increment yields an invalid date. The worst aspect of this “very bad code” is that it doesn’t look bad.

This kind of thinking leads to a demand for an initialization function that can’t be forgotten and for operations that are less likely to be overlooked. The basic tool for that is member functions, that is, functions declared as members of the class within the class body. For example:

// simple Date
// guarantee initialization with constructor
// provide some notational convenience
struct Date {
          int y, m, d;                            // year, month, day
          Date(int y, int m, int d);      // check for valid date and initialize
          void add_day(int n);           // increase the Date by n days
};

A member function with the same name as its class is special. It is called a constructor and will be used for initialization (“construction”) of objects of the class. It is an error — caught by the compiler — to forget to initialize an object of a class that has a constructor that requires an argument, and there is a special convenient syntax for doing such initialization:

Date my_birthday;                                  // error: my_birthday not initialized
Date today {12,24,2007};                        // oops! run-time error
Date last {2000,12,31};                            // OK (colloquial style)
Date next = {2014,2,14};                         // also OK (slightly verbose)
Date christmas = Date{1976,12,24};      // also OK (verbose style)

The attempt to declare my_birthday fails because we didn’t specify the required initial value. The attempt to declare today will pass the compiler, but the checking code in the constructor will catch the illegal date at run time ({12,24,2007} — there is no day 2007 of the 24th month of year 12).

The definition of last provides the initial value — the arguments required by Date’s constructor — as a { } list immediately after the name of the variable. That’s the most common style of initialization of variables of a class that has a constructor requiring arguments. We can also use the more verbose style where we explicitly create an object (here, Date{1976,12,24}) and then use that to initialize the variable using the = initializer syntax. Unless you actually like typing, you’ll soon tire of that.

We can now try to use our newly defined variables:

last.add_day(1);
add_day(2);                 // error: what date?

Note that the member function add_day() is called for a particular Date using the dot member-access notation. We’ll show how to define member functions in §9.4.4.

In C++98, people used parentheses to delimit the initializer list, so you will see a lot of code like this:

Date last(2000,12,31);            // OK (old colloquial style)

We prefer { } for initializer lists because that clearly indicates when initialization (construction) is done, and also because that notation is more widely useful. The notation can also be used for built-in types. For example:

int x {7};                  // OK (modern initializer list style)

Optionally, we can use a = before the { } list:

Date next = {2014,2,14};             // also OK (slightly verbose)

Some find this combination of older and new style more readable.

9.4.3 Keep details private

We still have a problem: What if someone forgets to use the member function add_day()? What if someone decides to change the month directly? After all, we “forgot” to provide a facility for that:

Date birthday {1960,12,31};        // December 31, 1960
++birthday.d;                                // ouch! Invalid date
                                                        // (birthday.d==32 makes today invalid)

Date today {1970,2,3};
today.m = 14;                               // ouch! Invalid date
                                                       // (today.m==14 makes today invalid)

Image

As long as we leave the representation of Date accessible to everybody, somebody will — by accident or design — mess it up; that is, someone will do something that produces an invalid value. In this case, we created a Date with a value that doesn’t correspond to a day on the calendar. Such invalid objects are time bombs; it is just a matter of time before someone innocently uses the invalid value and gets a run-time error or — usually worse — produces a bad result.

Such concerns lead us to conclude that the representation of Date should be inaccessible to users except through the public member functions that we supply. Here is a first cut:

// simple Date (control access)
class Date {
          int y, m, d;                                          // year, month, day
public:
          Date(int y, int m, int d);                   // check for valid date and initialize
          void add_day(int n);                        // increase the Date by n days
          int month() { return m; }
          int day() { return d; }
          int year() { return y; }
};

We can use it like this:

Date birthday {1970, 12, 30};                 // OK
birthday.m = 14;                                      // error: Date::m is private
cout << birthday.month() << '\n';         // we provided a way to read m

Image

The notion of a “valid Date” is an important special case of the idea of a valid value. We try to design our types so that values are guaranteed to be valid; that is, we hide the representation, provide a constructor that creates only valid objects, and design all member functions to expect valid values and leave only valid values behind when they return. The value of an object is often called its state, so the idea of a valid value is often referred to as a valid state of an object.

The alternative is for us to check for validity every time we use an object, or just hope that nobody left an invalid value lying around. Experience shows that “hoping” can lead to “pretty good” programs. However, producing “pretty good” programs that occasionally produce erroneous results and occasionally crash is no way to win friends and respect as a professional. We prefer to write code that can be demonstrated to be correct.

Image

A rule for what constitutes a valid value is called an invariant. The invariant for Date (“A Date must represent a day in the past, present, or future”) is unusually hard to state precisely: remember leap years, the Gregorian calendar, time zones, etc. However, for simple realistic uses of Dates we can do it. For example, if we are analyzing internet logs, we need not be bothered with the Gregorian, Julian, or Mayan calendars. If we can’t think of a good invariant, we are probably dealing with plain data. If so, use a struct.

9.4.4 Defining member functions

So far, we have looked at Date from the point of view of an interface designer and a user. But sooner or later, we have to implement those member functions. First, here is a subset of the Date class reorganized to suit the common style of providing the public interface first:

// simple Date (some people prefer implementation details last)
class Date {
public:
          Date(int y, int m, int d);    // constructor: check for valid date and initialize
          void add_day(int n);         // increase the Date by n days
          int month();
          // . . .
private:
          int y, m, d;                         // year, month, day
};

People put the public interface first because the interface is what most people are interested in. In principle, a user need not look at the implementation details. In reality, we are typically curious and have a quick look to see if the implementation looks reasonable and if the implementer used some technique that we could learn from. However, unless we are the implementers, we do tend to spend much more time with the public interface. The compiler doesn’t care about the order of class function and data members; it takes the declarations in any order you care to present them.

When we define a member outside its class, we need to say which class it is a member of. We do that using the class_name::member_name notation:

Date::Date(int yy, int mm, int dd)         // constructor
          :y{yy}, m{mm}, d{dd}                  // note: member initializers
{
}
void Date::add_day(int n)
{
          // . . .
}
int month()                         // oops: we forgot Date::
{
          return m;                 // not the member function, can’t access m
}

The :y{yy}, m{mm}, d{dd} notation is how we initialize members. It is called a (member) initializer list. We could have written

Date::Date(int yy, int mm, int dd)     // constructor
{
          y = yy;
          m = mm;
          d = dd;
}

but then we would in principle first have default initialized the members and then assigned values to them. We would then also open the possibility of accidentally using a member before it was initialized. The :y{yy}, m{mm}, d{dd} notation more directly expresses our intent. The distinction is exactly the same as the one between

int x;       // first define the variable x
// . . .
x = 2;      // later assign to x

and

int x {2};     // define and immediately initialize with 2

We can also define member functions right in the class definition:

// simple Date (some people prefer implementation details last)
class Date {
public:
          Date(int yy, int mm, int dd)
                    :y{yy}, m{mm}, d{dd}
          {
                    // . . .
          }

          void add_day(int n)
          {
                    // . . .
          }

          int month() { return m; }

          // . . .
private:
          int y, m, d;     // year, month, day
};

The first thing we notice is that the class declaration became larger and “messier.” In this example, the code for the constructor and add_day() could be a dozen or more lines each. This makes the class declaration several times larger and makes it harder to find the interface among the implementation details. Consequently, we don’t define large functions within a class declaration.

However, look at the definition of month(). That’s straightforward and shorter than the version that places Date::month() out of the class declaration. For such short, simple functions, we might consider writing the definition right in the class declaration.

Note that month() can refer to m even though m is defined after (below) month(). A member can refer to a function or data member of its class independently of where in the class that other member is declared. The rule that a name must be declared before it is used is relaxed within the limited scope of a class.

Writing the definition of a member function within the class definition has three effects:

Image

• The function will be inline; that is, the compiler will try to generate code for the function at each point of call rather than using function-call instructions to use common code. This can be a significant performance advantage for functions, such as month(), that hardly do anything but are used a lot.

• All uses of the class will have to be recompiled whenever we make a change to the body of an inlined function. If the function body is out of the class declaration, recompilation of users is needed only when the class declaration is itself changed. Not recompiling when the body is changed can be a huge advantage in large programs.

• The class definition gets larger. Consequently, it can be harder to find the members among the member function definitions.

Image

The obvious rule of thumb is: Don’t put member function bodies in the class declaration unless you know that you need the performance boost from inlining tiny functions. Large functions, say five or more lines of code, don’t benefit from inlining and make a class declaration harder to read. We rarely inline a function that consists of more than one or two expressions.

9.4.5 Referring to the current object

Consider a simple use of the Date class so far:

class Date {
          // . . .
          int month() { return m; }
          // . . .
private:
          int y, m, d;       // year, month, day
};

void f(Date d1, Date d2)
{
          cout << d1.month() << ' ' << d2.month() << '\n';
}

How does Date::month() know to return the value of d1.m in the first call and d2.m in the second? Look again at Date::month(); its declaration specifies no function argument! How does Date::month() know for which object it was called? A class member function, such as Date::month(), has an implicit argument which it uses to identify the object for which it is called. So in the first call, m correctly refers to d1.m and in the second call it refers to d2.m. See §17.10 for more uses of this implicit argument.

9.4.6 Reporting errors

Image

What do we do when we find an invalid date? Where in the code do we look for invalid dates? From §5.6, we know that the answer to the first question is “Throw an exception,” and the obvious place to look is where we first construct a Date. If we don’t create invalid Dates and also write our member functions correctly, we will never have a Date with an invalid value. So, we’ll prevent users from ever creating a Date with an invalid state:

// simple Date (prevent invalid dates)
class Date {
public:
          class Invalid { };                       // to be used as exception
          Date(int y, int m, int d);          // check for valid date and initialize
          // . . .
private:
          int y, m, d;                               // year, month, day
          bool is_valid();                        // return true if date is valid
};

We put the testing of validity into a separate is_valid() function because checking for validity is logically distinct from initialization and because we might want to have several constructors. As you can see, we can have private functions as well as private data:

Date::Date(int yy, int mm, int dd)
          : y{yy}, m{mm}, d{dd}                       // initialize data members
{
          if (!is_valid()) throw Invalid{};          // check for validity
}

bool Date::is_valid()                                   // return true if date is valid
{
          if (m<1 || 12<m) return false;
          // . . .
}

Given that definition of Date, we can write

void f(int x, int y)
try {
          Date dxy {2004,x,y};
          cout << dxy << '\n';              // see §9.8 for a declaration of <<
     dxy.add_day(2);
}
catch(Date::Invalid) {
          error("invalid date");          // error() defined in §5.6.3
}

We now know that << and add_day() will have a valid Date on which to operate.

Before completing the evolution of our Date class in §9.7, we’ll take a detour to describe a couple of general language facilities that we’ll need to do that well: enumerations and operator overloading.

9.5 Enumerations

Image

An enum (an enumeration) is a very simple user-defined type, specifying its set of values (its enumerators) as symbolic constants. For example:

enum class Month {
          jan=1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec
};

The “body” of an enumeration is simply a list of its enumerators. The class in enum class means that the enumerators are in the scope of the enumeration. That is, to refer to jan, we have to say Month::jan.

You can give a specific representation value for an enumerator, as we did for jan here, or leave it to the compiler to pick a suitable value. If you leave it to the compiler to pick, it’ll give each enumerator the value of the previous enumerator plus one. Thus, our definition of Month gave the months consecutive values starting with 1. We could equivalently have written

enum class Month {
          jan=1, feb=2, mar=3, apr=4, may=5, jun=6,
          jul=7, aug=8, sep=9, oct=10, nov=11, dec=12
};

However, that’s tedious and opens the opportunity for errors. In fact, we made two typing errors before getting this latest version right; it is better to let the compiler do simple, repetitive “mechanical” things. The compiler is better at such tasks than we are, and it doesn’t get bored.

If we don’t initialize the first enumerator, the count starts with 0. For example:

enum class Day {
          monday, tuesday, wednesday, thursday, friday, saturday, sunday
};

Here monday is represented as 0 and sunday is represented as 6. In practice, starting with 0 is often a good choice.

We can use our Month like this:

Month m = Month::feb;

Month m2 = feb;                     // error: feb is not in scope
m = 7;                                         // error: can’t assign an int to a Month
int n = m;                                   // error: can’t assign a Month to an int
Month mm = Month(7);         // convert int to Month (unchecked)

Month is a separate type from its “underlying type” int. Every Month has an equivalent integer value, but most ints do not have a Month equivalent. For example, we really do want this initialization to fail:

Month bad = 9999;     // error: can’t convert an int to a Month

Image

If you insist on using the Month(9999) notation, on your head be it! In many cases, C++ will not try to stop a programmer from doing something potentially silly when the programmer explicitly insists; after all, the programmer might actually know better. Note that you cannot use theMonth{9999} notation because that would allow only values that could be used in an initialization of a Month, and ints cannot.

Unfortunately, we cannot define a constructor for an enumeration to check initializer values, but it is trivial to write a simple checking function:

Month int_to_month(int x)
{
          if (x<int(Month::jan) || int(Month::dec)<x) error("bad month");
          return Month(x);
}

We use the int(Month::jan) notation to get the int representation of Month::jan. Given that, we can write

void f(int m)
{
          Month mm = int_to_month(m);
          // . . .
}

What do we use enumerations for? Basically, an enumeration is useful whenever we need a set of related named integer constants. That happens all the time when we try to represent sets of alternatives (up, down; yes, no, maybe; on, off; n, ne, e, se, s, sw, w, nw) or distinctive values (red,blue, green, yellow, maroon, crimson, black).

9.5.1 “Plain” enumerations

In addition to the enum classes, also known as scoped enumerations, there are “plain” enumerations that differ from scoped enumerations by implicitly “exporting” their enumerators to the scope of the enumeration and allowing implicit conversions to int. For example:

enum Month {                               // note: no “class”
          jan=1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec
};

Month m = feb;                            // OK: feb in scope
Month m2 = Month::feb;          // also OK
m = 7;                                             // error: can’t assign an int to a Month
int n = m;                                  // OK: we can assign a Month to an int
Month mm = Month(7);        // convert int to Month (unchecked)

Obviously, “plain” enums are less strict than enum classes. Their enumerators can “pollute” the scope in which their enumerator is defined. That can be a convenience, but it occasionally leads to surprises. For example, if you try to use this Month together with the iostream formatting mechanisms (§11.2.1), you will find that dec for December clashes with dec for decimal.

Similarly, having an enumeration value convert to int can be a convenience (it saves us from being explicit when we want a conversion to int), but occasionally it leads to surprises. For example:

void my_code(Month m)
{
          If (m==17) do_something();                            // huh: 17th month?
          If (m==monday) do_something_else();        // huh: compare month to
                                                                                 // Monday?
}

If Month is an enum class, neither condition will compile. If monday is an enumerator of a “plain” enum, rather than an enum class, the comparison of a month to Monday would succeed, most likely with undesirable results.

Prefer the simpler and safer enum classes to “plain” enums, but expect to find “plain” enums in older code: enum classes are new in C++11.

9.6 Operator overloading

You can define almost all C++ operators for class or enumeration operands. That’s often called operator overloading. We use it when we want to provide conventional notation for a type we design. For example:

enum class Month {
          Jan=1, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec
};

Month operator++(Month& m)                                 // prefix increment operator
{
          m = (m==Dec) ? Jan : Month(int(m)+1);       // “wrap around”
          return m;
}

The ? : construct is an “arithmetic if”: m becomes Jan if (m==Dec) and Month(int(m)+1) otherwise. It is a reasonably elegant way of expressing the fact that months “wrap around” after December. The Month type can now be used like this:

Month m = Sep;
++m;       // m becomes Oct
++m;       // m becomes Nov
++m;       // m becomes Dec
++m;       // m becomes Jan (“wrap around”)

You might not think that incrementing a Month is common enough to warrant a special operator. That may be so, but how about an output operator? We can define one like this:

vector<string> month_tbl;

ostream& operator<<(ostream& os, Month m)
{
          return os << month_tbl[int(m)];
}

This assumes that month_tbl has been initialized somewhere so that (for example) month_tbl[int(Month::mar)] is "March" or some other suitable name for that month; see §10.11.3.

You can define just about any operator provided by C++ for your own types, but only existing operators, such as +, –, *, /, %, [ ], ( ), ^, !, &, <, <=, >, and >=. You cannot define your own operators; you might like to have ** or $= as operators in your program, but C++ won’t let you. You can define operators only with their conventional number of operands; for example, you can define unary –, but not unary <= (less than or equal), and binary +, but not binary ! (not). Basically, the language allows you to use the existing syntax for the types you define, but not to extend that syntax.

Image

An overloaded operator must have at least one user-defined type as operand:

int operator+(int,int);      // error: you can’t overload built-in +
Vector operator+(const Vector&, const Vector &);      // OK
Vector operator+=(const Vector&, int);                          // OK

Image

It is generally a good idea not to define operators for a type unless you are really certain that it makes a big positive change to your code. Also, define operators only with their conventional meaning: + should be addition, binary * multiplication, [ ] access, ( ) call, etc. This is just advice, not a language rule, but it is good advice: conventional use of operators, such as + for addition, can significantly help us understand a program. After all, such use is the result of hundreds of years of experience with mathematical notation. Conversely, obscure operators and unconventional use of operators can be a significant distraction and a source of errors. We will not elaborate on this point. Instead, in the following chapters, we will simply use operator overloading in a few places where we consider it appropriate.

Note that the most interesting operators to overload aren’t +, –, *, and / as people often assume, but =, ==, !=, <, [ ] (subscript), and ( ) (call).

9.7 Class interfaces

We have argued that the public interface and the implementation parts of a class should be separated. As long as we leave open the possibility of using structs for types that are “plain old data,” few professionals would disagree. However, how do we design a good interface? What distinguishes a good public interface from a mess? Part of that answer can be given only by example, but there are a few general principles that we can list and that are given some support in C++:

Image

• Keep interfaces complete.

• Keep interfaces minimal.

• Provide constructors.

• Support copying (or prohibit it) (see §14.2.4).

• Use types to provide good argument checking.

• Identify nonmodifying member functions (see §9.7.4).

• Free all resources in the destructor (see §17.5).

See also §5.5 (how to detect and report run-time errors).

The first two principles can be summarized as “Keep the interface as small as possible, but no smaller.” We want our interface to be small because a small interface is easy to learn and easy to remember, and the implementer doesn’t waste a lot of time implementing unnecessary and rarely used facilities. A small interface also means that when something is wrong, there are only a few functions to check to find the problem. On average, the more public member functions are, the harder it is to find bugs — and please don’t get us started on the complexities of debugging classes with public data. But of course, we want a complete interface; otherwise, it would be useless. We couldn’t use an interface that didn’t allow us to do all we really needed.

Let’s look at the other — less abstract and more directly supported — ideals.

9.7.1 Argument types

When we defined the constructor for Date in §9.4.3, we used three ints as the arguments. That caused some problems:

Date d1 {4,5,2005};   // oops: year 4, day 2005
Date d2 {2005,4,5};   // April 5 or May 4?

The first problem (an illegal day of the month) is easily dealt with by a test in the constructor. However, the second (a month vs. day-of-the-month confusion) can’t be caught by code written by the user. The second problem is simply that the conventions for writing month and day-in-month differ; for example, 4/5 is April 5 in the United States and May 4 in England. Since we can’t calculate our way out of this, we must do something else. The obvious solution is to use a Month type:

          enum class Month {
                    jan=1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec
                    };

// simple Date (use Month type)
class Date {
public:
          Date(int y, Month m, int d);       // check for valid date and initialize
          // . . .
private:
          int y;                                               // year
          Month m;
          int d;                                              // day
};

When we use a Month type, the compiler will catch us if we swap month and day, and using an enumeration as the Month type also gives us symbolic names to use. It is usually easier to read and write symbolic names than to play around with numbers, and therefore less error-prone:

Date dx1 {1998, 4, 3};                            // error: 2nd argument not a Month
Date dx2 {1998, 4, Month::mar};       // error: 2nd argument not a Month
Date dx2 {4, Month::mar, 1998};       // oops: run-time error: day 1998
Date dx2 {Month::mar, 4, 1998};       // error: 2nd argument not a Month
Date dx3 {1998, Month::mar, 30};     // OK

This takes care of most “accidents.” Note the use of the qualification of the enumerator mar with the enumeration name: Month::mar. We don’t say Month.mar because Month isn’t an object (it’s a type) and mar isn’t a data member (it’s an enumerator — a symbolic constant). Use :: after the name of a class, enumeration, or namespace (§8.7) and . (dot) after an object name.

Image

When we have a choice, we catch errors at compile time rather than at run time. We prefer for the compiler to find the error rather than for us to try to figure out exactly where in the code a problem occurred. Also, errors caught at compile time don’t require checking code to be written and executed.

Thinking like that, could we catch the swap of the day of the month and the year also? We could, but the solution is not as simple or as elegant as for Month; after all, there was a year 4 and you might want to represent it. Even if we restricted ourselves to modern times there would probably be too many relevant years for us to list them all in an enumeration.

Probably the best we could do (without knowing quite a lot about the intended use of Date) would be something like this:

class Year {                                                         // year in [min:max) range
          static const int min = 1800;
          static const int max = 2200;
public:
          class Invalid { };
          Year(int x) : y{x} { if (x<min || max<=x) throw Invalid{}; }
          int year() { return y; }
private:
          int y;
};

class Date {
public:
          Date(Year y, Month m, int d);            // check for valid date and initialize
          // . . .
private:
          Year y;
          Month m;
          int d;       // day
};

Now we get

Date dx1 {Year{1998}, 4, 3};                            // error: 2nd argument not a Month
Date dx2 {Year{1998}, 4, Month::mar};       // error: 2nd argument not a Month
Date dx2 {4, Month::mar, Year{1998}};       // error: 1st argument not a Year
Date dx2 {Month::mar, 4, Year{1998}};       // error: 2nd argument not a Month
Date dx3 {Year{1998}, Month::mar, 30};     // OK

This weird and unlikely error would still not be caught until run time:

Date dx2 {Year{4}, Month::mar, 1998};    // run-time error: Year::Invalid

Is the extra work and notation to get years checked worthwhile? Naturally, that depends on the constraints on the kind of problem you are solving using Date, but in this case we doubt it and won’t use class Year as we go along.

Image

When we program, we always have to ask ourselves what is good enough for a given application. We usually don’t have the luxury of being able to search “forever” for the perfect solution after we have already found one that is good enough. Search further, and we might even come up with something that’s so elaborate that it is worse than the simple early solution. This is one meaning of the saying “The best is the enemy of the good” (Voltaire).

Image

Note the use of static const in the definitions of min and max. This is the way we define symbolic constants of integer types within classes. For a class member, we use static to make sure that there is just one copy of the value in the program, rather than one per object of the class. In this case, because the initializer is a constant expression, we could have used constexpr instead of const.

9.7.2 Copying

We always have to create objects; that is, we must always consider initialization and constructors. Arguably they are the most important members of a class: to write them, you have to decide what it takes to initialize an object and what it means for a value to be valid (what is the invariant?). Just thinking about initialization will help you avoid errors.

The next thing to consider is often: Can we copy our objects? And if so, how do we copy them?

For Date or Month, the answer is that we obviously want to copy objects of that type and that the meaning of copy is trivial: just copy all of the members. Actually, this is the default case. So as long as you don’t say anything else, the compiler will do exactly that. For example, if you copy a Date as an initializer or right-hand side of an assignment, all its members are copied:

Date holiday {1978, Month::jul, 4};           // initialization
Date d2 = holiday;
Date d3 = Date{1978, Month::jul, 4};
holiday = Date{1978, Month::dec, 24};   // assignment
d3 = holiday;

This will all work as expected. The Date{1978, Month::dec, 24} notation makes the appropriate unnamed Date object, which you can then use appropriately. For example:

cout << Date{1978, Month::dec, 24};

This is a use of a constructor that acts much as a literal for a class type. It often comes in as a handy alternative to first defining a variable or const and then using it once.

What if we don’t want the default meaning of copying? We can either define our own (see §18.3) or delete the copy constructor and copy assignment (see §14.2.4).

9.7.3 Default constructors

Image

Uninitialized variables can be a serious source of errors. To counter that problem, we have the notion of a constructor to guarantee that every object of a class is initialized. For example, we declared the constructor Date::Date(int,Month,int) to ensure that every Date is properly initialized. In the case of Date, that means that the programmer must supply three arguments of the right types. For example:

Date d0;                                        // error: no initializer
Date d1 {};                                    // error: empty initializer
Date d2 {1998};                            // error: too few arguments
Date d3 {1,2,3,4};                        // error: too many arguments
Date d4 {1,"jan",2};                    // error: wrong argument type
Date d5 {1,Month::jan,2};        // OK: use the three-argument constructor
Date d6 {d5};                                // OK: use the copy constructor

Note that even though we defined a constructor for Date, we can still copy Dates.

Many classes have a good notion of a default value; that is, there is an obvious answer to the question “What value should it have if I didn’t give it an initializer?” For example:

string s1;                         // default value: the empty string " "
vector<string> v1;        // default value: the empty vector; no elements

This looks reasonable. It even works the way the comments indicate. That is achieved by giving vector and string default constructors that implicitly provide the desired initialization.

For a type T, T{} is the notation for the default value, as defined by the default constructor, so we could write

Image

string s1 = string{};                                      // default value: the empty string " "
vector<string> v1 = vector<string>{};     // default value: the empty vector;
                                                                              // no elements

However, we prefer the equivalent and colloquial

string s1;                           // default value: the empty string " "
vector<string> v1;          // default value: the empty vector; no elements

For built-in types, such as int and double, the default constructor notation means 0, so int{} is a complicated way of saying 0, and double{} a long-winded way of saying 0.0.

Using a default constructor is not just a matter of looks. Imagine that we could have an uninitialized string or vector.

string s;
for (int i=0; i<s.size(); ++i)     // oops: loop an undefined number of times
          s[i] = toupper(s[i]);        // oops: read and write a random memory location

vector<string> v;
v.push_back("bad");             // oops: write to random address

If the values of s and v were genuinely undefined, s and v would have no notion of how many elements they contained or (using the common implementation techniques; see §17.5) where those elements were supposed to be stored. The results would be use of random addresses — and that can lead to the worst kind of errors. Basically, without a constructor, we cannot establish an invariant — we cannot ensure that the values in those variables are valid (§9.4.3). We must insist that such variables are initialized. We could insist on an initializer and then write

string s1 = "";
vector<string> v1 {};

But we don’t think that’s particularly pretty. For string, "" is rather obvious for “empty string.” For vector, { } is pretty for a vector with no elements. However, for many types, it is not easy to find a reasonable notation for a default value. For many types, it is better to define a constructor that gives meaning to the creation of an object without an explicit initializer. Such a constructor takes no arguments and is called a default constructor.

There isn’t an obvious default value for dates. That’s why we haven’t defined a default constructor for Date so far, but let’s provide one (just to show we can):

class Date {
public:
          // . . .
          Date();         // default constructor
          // . . .
private:
          int y;
          Month m;
          int d;
};

We have to pick a default date. The first day of the 21st century might be a reasonable choice:

Date::Date()
          :y{2001}, m{Month::jan}, d{1}
{
}

Instead of placing the default values for members in the constructor, we could place them on the members themselves:

class Date {
public:
          // . . .
          Date();                                                 // default constructor
                    Date(year, Month, day);
                    Date(int y);                               // January 1 of year y
          // . . .
private:
          int y {2001};
          Month m {Month::jan};
          int d {1};
};

That way, the default values are available to every constructor. For example:

Date::Date(int y)                                       // January 1 of year y
          :y{yy}
{
          if (!is_valid()) throw Invalid{};      // check for validity
}

Because Date(int) does not explicitly initialize the month (m) or the day (d), the specified initializers (Month::jan and 1) are implicitly used. An initializer for a class member specified as part of the member declaration is called an in-class initializer.

Image

If we didn’t like to build the default value right into the constructor code, we could use a constant (or a variable). To avoid a global variable and its associated initialization problems, we use the technique from §8.6.2:

const Date& default_date()
{
          static Date dd {2001,Month::jan,1};
          return dd;
}

We used static to get a variable (dd) that is created only once, rather than each time default_date() is called, and initialized the first time default_date() is called. Given default_date(), it is trivial to define a default constructor for Date:

Date::Date()
          :y{default_date().year()},
          m{default_date().month()},
          d{default_date().day()}
{
}

Note that the default constructor does not need to check its value; the constructor for default_date already did that. Given this default Date constructor, we can now define nonempty vectors of Dates without listing element values:

vector<Date> birthdays(10);      // ten elements with the default Date value,
                                                              // Date{}

Without the default constructor, we would have had to be explicit:

vector<Date> birthdays(10,default_date());   // ten default Dates

vector<Date> birthdays2 = {                             // ten default Dates
          default_date(), default_date(), default_date(), default_date(), default_
                    date(),
          default_date(), default_date(), default_date(), default_date(), default_
                   date()
};

We use parentheses, ( ), when specifying the element counts for a vector, rather than the { } initializer-list notation, to avoid confusion in the case of a vector<int> (§18.2).

9.7.4 const member functions

Some variables are meant to be changed — that’s why we call them “variables” — but some are not; that is, we have “variables” representing immutable values. Those, we typically call constants or just consts. Consider:

void some_function(Date& d, const Date& start_of_term)
{
          int a = d.day();   // OK
          int b = start_of_term.day();         // should be OK (why?)
          d.add_day(3);                            // fine
          start_of_term.add_day(3);      // error
}

Here we intend d to be mutable, but start_of_term to be immutable; it is not acceptable for some_function() to change start_of_term. How would the compiler know that? It knows because we told it by declaring start_of_term const. So far, so good, but then why is it OK to read the day ofstart_of_term using day()? As the definition of Date stands so far, start_of_term.day() is an error because the compiler does not know that day() doesn’t change its Date. We never told it, so the compiler assumes that day() may modify its Date and reports an error.

We can deal with this problem by classifying operations on a class as modifying and nonmodifying. That’s a pretty fundamental distinction that helps us understand a class, but it also has a very practical importance: operations that do not modify the object can be invoked for const objects. For example:

Image

class Date {
public:
          // . . .
          int day() const;                         // const member: can’t modify the object
          Month month() const;           // const member: can’t modify the object
          int year() const;                       // const member: can’t modify the object

          void add_day(int n);              // non-const member: can modify the object
          void add_month(int n);        // non-const member: can modify the object
          void add_year(int n);            // non-const member: can modify the object
private:
          int y;                                         // year
          Month m;
          int d;                                        // day of month
};

Date d {2000, Month::jan, 20};
const Date cd {2001, Month::feb, 21};

cout << d.day() << " — " << cd.day() << '\n';      // OK
d.add_day(1);                                                             // OK
cd.add_day(1);                                                           // error: cd is a const

We use const right after the argument list in a member function declaration to indicate that the member function can be called for a const object. Once we have declared a member function const, the compiler holds us to our promise not to modify the object. For example:

int Date::day() const
{
          ++d;       // error: attempt to change object from const member function
          return d;
}

Naturally, we don’t usually try to “cheat” in this way. What the compiler provides for the class implementer is primarily protection against accident, which is particularly useful for more complex code.

9.7.5 Members and “helper functions”

Image

When we design our interfaces to be minimal (though complete), we have to leave out lots of operations that are merely useful. A function that can be simply, elegantly, and efficiently implemented as a freestanding function (that is, as a nonmember function) should be implemented outside the class. That way, a bug in that function cannot directly corrupt the data in a class object. Not accessing the representation is important because the usual debug technique is “Round up the usual suspects”; that is, when something goes wrong with a class, we first look at the functions that directly access the representation: one of those almost certainly did it. If there are a dozen such functions, we will be much happier than if there were 50.

Fifty functions for a Date class! You must wonder if we are kidding. We are not: a few years ago I surveyed a number of commercially used Date libraries and found them full of functions like next_Sunday(), next_workday(), etc. Fifty is not an unreasonable number for a class designed for the convenience of the users rather than for ease of comprehension, implementation, and maintenance.

Note also that if the representation changes, only the functions that directly access the representation need to be rewritten. That’s another strong practical reason for keeping interfaces minimal. In our Date example, we might decide that an integer representing the number of days since January 1, 1900, is a much better representation for our uses than (year,month,day). Only the member functions would have to be changed.

Here are some examples of helper functions:

Date next_Sunday(const Date& d)
{
          // access d using d.day(), d.month(), and d.year()
          // make new Date to return
}

Date next_weekday(const Date& d) { /* . . . */ }

bool leapyear(int y) { /* . . . */ }

bool operator==(const Date& a, const Date& b)
{
          return a.year()==b.year()
                    && a.month()==b.month()
                    && a.day()==b.day();
}

bool operator!=(const Date& a, const Date& b)
{
          return !(a==b);
}

Image

Helper functions are also called convenience functionsauxiliary functions, and many other things. The distinction between these functions and other nonmember functions is logical; that is, “helper function” is a design concept, not a programming language concept. The helper functions often take arguments of the classes that they are helpers of. There are exceptions, though: note leapyear(). Often, we use namespaces to identify a group of helper functions; see §8.7:

namespace Chrono {
          enum class Month { /* ... */ };
          class Date { /* . . . */ };
          bool is_date(int y, Month m, int d);         // true for valid date
          Date next_Sunday(const Date& d) { /* . . . */ }
          Date next_weekday(const Date& d) { /* . . . */ }
          bool leapyear(int y) { /* . . . */ }                   // see exercise 10
          bool operator==(const Date& a, const Date& b) { /* . . . */ }
// . . .
}

Note the == and != functions. They are typical helpers. For many classes, == and != make obvious sense, but since they don’t make sense for all classes, the compiler can’t write them for you the way it writes the copy constructor and copy assignment.

Note also that we introduced a helper function is_date(). That function replaces Date::is_valid() because checking whether a date is valid is largely independent of the representation of a Date. For example, we don’t need to know how Date objects are represented to know that “January 30, 2008” is a valid date and “February 30, 2008” is not. There still may be aspects of a date that depend on the representation (e.g., can we represent “January 30, 1066”?), but (if necessary) Date’s constructor can take care of that.

9.8 The Date class

So, let’s just put it all together and see what that Date class might look like when we combine all of the ideas/concerns. Where a function’s body is just a . . . comment, the actual implementation is tricky (please don’t try to write those just yet). First we place the declarations in a headerChrono.h:

// file Chrono.h

namespace Chrono {

enum class Month {
          jan=1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec
};

class Date {
public:
          class Invalid { };                               // to throw as exception

          Date(int y, Month m, int d);         // check for valid date and initialize
          Date();                                              // default constructor
          // the default copy operations are fine

          // nonmodifying operations:
          int day() const { return d; }
          Month month() const { return m; }
          int year() const { return y; }

          // modifying operations:
          void add_day(int n);
          void add_month(int n);
          void add_year(int n);
private:
          int y;
          Month m;
          int d;
};

bool is_date(int y, Month m, int d);  // true for valid date
bool leapyear(int y);           // true if y is a leap year

bool operator==(const Date& a, const Date& b);
bool operator!=(const Date& a, const Date& b);

ostream& operator<<(ostream& os, const Date& d);

istream& operator>>(istream& is, Date& dd);

}                                              // Chrono

The definitions go into Chrono.cpp:

// Chrono.cpp
#include "Chrono.h"

namespace Chrono {
// member function definitions:

Date::Date(int yy, Month mm, int dd)
          : y{yy}, m{mm}, d{dd}
{
          if (!is_date(yy,mm,dd)) throw Invalid{};
}

const Date& default_date()
{
          static Date dd {2001,Month::jan,1};   // start of 21st century
          return dd;
}

Date::Date()
          :y{default_date().year()},
          m{default_date().month()},
          d{default_date().day()}
{
}

void Date:: add_day(int n)
{
          // . . .
}

void Date::add_month(int n)
{
          // . . .
}

void Date::add_year(int n)
{
          if (m==feb && d==29 && !leapyear(y+n)) {             // beware of leap years!
                    m = mar;                                        // use March 1 instead of February 29
                    d = 1;
          }
          y+=n;
}
// helper functions:

bool is_date(int y, Month m, int d)
{
          // assume that y is valid

          if (d<=0) return false;                        // d must be positive
          if (m<Month::jan || Month::dec<m) return false;

          int days_in_month = 31;                  // most months have 31 days

          switch (m) {
          case Month::feb:                              // the length of February varies
                    days_in_month = (leapyear(y))?29:28;
                    break;
          case Month::apr: case Month::jun: case Month::sep: case Month::nov:
                    days_in_month = 30;               // the rest have 30 days
                    break;
          }

          if (days_in_month<d) return false;

          return true;
}

bool leapyear(int y)
{
          // see exercise 10
}

bool operator==(const Date& a, const Date& b)
{
          return a.year()==b.year()
                    && a.month()==b.month()
                    && a.day()==b.day();
}

bool operator!=(const Date& a, const Date& b)
{
          return !(a==b);
}
ostream& operator<<(ostream& os, const Date& d)
{
          return os << '(' << d.year()
                              << ',' << d.month()
                              << ',' << d.day() << ')';
}

istream& operator>>(istream& is, Date& dd)
{
          int y, m, d;
          char ch1, ch2, ch3, ch4;
          is >> ch1 >> y >> ch2 >> m >> ch3 >> d >> ch4;
          if (!is) return is;
          if (ch1!= '(' || ch2!=',' || ch3!=',' || ch4!=')') {       // oops: format error
                    is.clear(ios_base::failbit);                            // set the fail bit
          return is;
          }

          dd = Date(y, Month(m),d);                                   // update dd

          return is;
}

enum class Day {
          sunday, monday, tuesday, wednesday, thursday, friday, saturday
};

Day day_of_week(const Date& d)
{
          // . . .
}

Date next_Sunday(const Date& d)
{
          // ...
}

Date next_weekday(const Date& d)
{
          // . . .
}

}        // Chrono

The functions implementing >> and << for Date will be explained in detail in §10.8 and §10.9.

Image Drill

This drill simply involves getting the sequence of versions of Date to work. For each version define a Date called today initialized to June 25, 1978. Then, define a Date called tomorrow and give it a value by copying today into it and increasing its day by one using add_day(). Finally, outputtoday and tomorrow using a << defined as in §9.8.

Your check for a valid date may be very simple. Feel free to ignore leap years. However, don’t accept a month that is not in the [1,12] range or day of the month that is not in the [1,31] range. Test each version with at least one invalid date (e.g., 2004, 13, –5).

1. The version from §9.4.1

2. The version from §9.4.2

3. The version from §9.4.3

4. The version from §9.7.1

5. The version from §9.7.4

Review

1. What are the two parts of a class, as described in the chapter?

2. What is the difference between the interface and the implementation in a class?

3. What are the limitations and problems of the original Date struct that is created in the chapter?

4. Why is a constructor used for the Date type instead of an init_day() function?

5. What is an invariant? Give examples.

6. When should functions be put in the class definition, and when should they be defined outside the class? Why?

7. When should operator overloading be used in a program? Give a list of operators that you might want to overload (each with a reason).

8. Why should the public interface to a class be as small as possible?

9. What does adding const to a member function do?

10. Why are “helper functions” best placed outside the class definition?

Terms

built-in types

class

const

constructor

destructor

enum

enumeration

enumerator

helper function

implementation

in-class initializer

inlining

interface

invariant

representation

struct

structure

user-defined types

valid state

Exercises

1. List sets of plausible operations for the examples of real-world objects in §9.1 (such as toaster).

2. Design and implement a Name_pairs class holding (name,age) pairs where name is a string and age is a double. Represent that as a vector<string> (called name) and a vector<double> (called age) member. Provide an input operation read_names() that reads a series of names. Provide aread_ages() operation that prompts the user for an age for each name. Provide a print() operation that prints out the (name[i],age[i]) pairs (one per line) in the order determined by the name vector. Provide a sort() operation that sorts the name vector in alphabetical order and reorganizes the age vector to match. Implement all “operations” as member functions. Test the class (of course: test early and often).

3. Replace Name_pair::print() with a (global) operator << and define == and != for Name_pairs.

4. Look at the headache-inducing last example of §8.4. Indent it properly and explain the meaning of each construct. Note that the example doesn’t do anything meaningful; it is pure obfuscation.

5. This exercise and the next few require you to design and implement a Book class, such as you can imagine as part of software for a library. Class Book should have members for the ISBN, title, author, and copyright date. Also store data on whether or not the book is checked out. Create functions for returning those data values. Create functions for checking a book in and out. Do simple validation of data entered into a Book; for example, accept ISBNs only of the form n-n-n-x where n is an integer and x is a digit or a letter. Store an ISBN as a string.

6. Add operators for the Book class. Have the == operator check whether the ISBN numbers are the same for two books. Have != also compare the ISBN numbers. Have a << print out the title, author, and ISBN on separate lines.

7. Create an enumerated type for the Book class called Genre. Have the types be fiction, nonfiction, periodical, biography, and children. Give each book a Genre and make appropriate changes to the Book constructor and member functions.

8. Create a Patron class for the library. The class will have a user’s name, library card number, and library fees (if owed). Have functions that access this data, as well as a function to set the fee of the user. Have a helper function that returns a Boolean (bool) depending on whether or not the user owes a fee.

9. Create a Library class. Include vectors of Books and Patrons. Include a struct called Transaction. Have it include a Book, a Patron, and a Date from the chapter. Make a vector of Transactions. Create functions to add books to the library, add patrons to the library, and check out books. Whenever a user checks out a book, have the library make sure that both the user and the book are in the library. If they aren’t, report an error. Then check to make sure that the user owes no fees. If the user does, report an error. If not, create a Transaction, and place it in the vector ofTransactions. Also write a function that will return a vector that contains the names of all Patrons who owe fees.

10. Implement leapyear() from §9.8.

11. Design and implement a set of useful helper functions for the Date class with functions such as next_workday() (assume that any day that is not a Saturday or a Sunday is a workday) and week_of_year() (assume that week 1 is the week with January 1 in it and that the first day of a week is a Sunday).

12. Change the representation of a Date to be the number of days since January 1, 1970 (known as day 0), represented as a long int, and re-implement the functions from §9.8. Be sure to reject dates outside the range we can represent that way (feel free to reject days before day 0, i.e., no negative days).

13. Design and implement a rational number class, Rational. A rational number has two parts: a numerator and a denominator, for example, 5/6 (five-sixths, also known as approximately .83333). Look up the definition if you need to. Provide assignment, addition, subtraction, multiplication, division, and equality operators. Also, provide a conversion to double. Why would people want to use a Rational class?

14. Design and implement a Money class for calculations involving dollars and cents where arithmetic has to be accurate to the last cent using the 4/5 rounding rule (.5 of a cent rounds up; anything less than .5 rounds down). Represent a monetary amount as a number of cents in a long int, but input and output as dollars and cents, e.g., $123.45. Do not worry about amounts that don’t fit into a long int.

15. Refine the Money class by adding a currency (given as a constructor argument). Accept a floating-point initializer as long as it can be exactly represented as a long int. Don’t accept illegal operations. For example, Money*Money doesn’t make sense, and USD1.23+DKK5.00 makes sense only if you provide a conversion table defining the conversion factor between U.S. dollars (USD) and Danish kroner (DKK).

16. Define an input operator (>>) that reads monetary amounts with currency denominations, such as USD1.23 and DKK5.00, into a Money variable. Also define a corresponding output operator (>>).

17. Give an example of a calculation where a Rational gives a mathematically better result than Money.

18. Give an example of a calculation where a Rational gives a mathematically better result than double.

Postscript

There is a lot to user-defined types, much more than we have presented here. User-defined types, especially classes, are the heart of C++ and the key to many of the most effective design techniques. Most of the rest of the book is about the design and use of classes. A class — or a set of classes — is the mechanism through which we represent our concepts in code. Here we primarily introduced the language-technical aspects of classes; elsewhere we focus on how to elegantly express useful ideas as