Code Capsule

A C++ Date Class, Part 2

Chuck Allison


In last month's capsule I presented the beginnings of a simple date class. In addition to providing a member function to calculate the interval between two dates, this class illustrated the following features of C++:

In this month's installment I will add relational operators, input/output operations, and the capability to get the current date, while demonstrating these features:

When using dates you often need to determine if one precedes another. I will add the member function

int compare(const Date& d2) const;
to the date class (see
Listing 1) .

Date::compare behaves like strcmp — it returns a negative integer if the current object (*this) precedes d2, 0 if both represent the same date, and a positive integer otherwise (see Listing 2 for the implementation and Listing 3 for a sample program). For those of you familiar with qsort from the Standard C library, you can use Date::compare to sort dates just like you use strcmp to sort strings. Here is a compare function to pass to qsort:

#include "date.h"
int datecmp(const void *p1, const void *p2)
{
   const Date
   *d1p = (const Date *) p1,
   *d2p = (const Date *) p2;
   return d1p->compare(*d2p);
}
(Next month's CODE CAPSULE will cover qsort).

Operator Overloading

Most of the time, it is more convenient to have relational operators, for example

if (d1 < d2)
  // do something appropriate..
Adding a less-than operator is trivial using Date::compare — just insert the following in-line member function into the class definition:

int operator<(const Date& d2) const
{return compare(d2) < 0);
The compiler translates each occurrence of an expression such as

d1 < d2
into the function call

d1.operator<(d2)
Listing 4 has the class definition with all six relational operators and the updated sample program is in Listing 5.

Since the functionality of Date::interval is like a subtraction (it gives us the difference between two dates), it would seem natural to rename it as Date::operator-. Before doing this, take a closer look at the semantics of the statement

a = b - c;
No matter the type of the variables, the following should hold:

a is a distinct object created by the subtraction, and b - c == - (c - b)

I will use the convention that a "positive" Date object has all positive data members, and conversely for a "negative" date (mixed signs are not allowed). In Listing 7, I have replaced Date::interval with Date::operator- (const Date&), which affixes the proper signs to the data members and returns a newly-constructed Date object. The new class definition in Listing 6 also contains a unary minus operator, which also has the name Date::operator-, but takes no arguments. The compiler will transform the statements

d1 - d2;
-d1;
respectively into

d1.operator-(d2); // Calls Date::operator-(const Date&)
d1.operator-();   // Calls Date::operator-()
A sample program using these new member functions is in Listing 8.

Stream I/O

One thing remains before I can say that a Date object has the look and feel of a built-in type — input/output support. C++ supplies stream objects that handle I/O for standard types. For example, the output from the program

#include <iostream.h>

main()
{
  int i;
  cout << "Enter an integer: ";
  cin >> i;
  cout << "You typed " << i << endl;
  return 0;
}
will be something like

Enter an integer: 5
You typed 5
cout is an output stream (class ostreom) supplied by the C++ streams library and cin is an input stream (class istream), which are associated by default with standard output and standard input, respectively. When the compiler sees the expression

cout << "Enter an integer: "
it replaces it with

cout.operator<<("Enter an integer: ")
which calls the member function ostream::operator<<(const char *). Likewise, the expression

cout << i
where i is an integer calls the function

ostream::-operator<<(int).
endl is a special stream directive (called a manipulator) which outputs a newline and flushes the output buffer. Output items can be chained together:

cout << "You typed " << i
because ostream::operator<< returns a reference to the stream itself. The above statement becomes

(cout.operator<<("You typed ")).operator<<(i)
To accommodate output of Date objects, then, you will need a global function that sends the printed representation to a given output stream, and then returns a reference to that stream:

ostream& operator<<(ostream& os, const Date& d)
{
   os << d.get_month() << '/'
   << d.get_day() << '/'
   << d.get_year();
   return os;
}
This of course can't be a member function, since the stream (not the object being output) always appears to the left of the stream insertion operator.

Friends

For efficiency, it is customary to give operator<< access to the private data members of an object. (Most implementations of a class provide the associated I/O operators too, so it seems safe to break the encapsulation boundary in this case.) To bypass the restriction on access to private members, you need to declare operator<< to be a friend of the Date class by adding the following statement to the class definition:

friend ostream& operator<<(ostream&, const Date&);
This declaration appears in the new class definition in
Listing 9, along with the corresponding friend declaration for the input function, operator>>. The implementation for these functions is in Listing 10, and Listing 11 has a sample program.

Static Members

A class in C++ defines a scope. This is why the function Date::compare would not conflict with a global function named compare (even if the arguments and return value were of the same type). Now consider the array dtab[] in the implementation file. dtab's static storage class makes it private to the file, but it really belongs to the class, not to the file. If I chose to spread the Date member functions across multiple files, I would be forced to group those that needed access to dtab together in the same file.

A better solution is to make dtab a static member of the Date class. Static members belong to the whole class, not to a single object. This means that only one copy of dtab exists, and is shared by all objects of the class. Making the function isleap static allows you to call it without an associated object, i.e., you just need to say

isleap(y);
instead of

d.isleap(y);
To make isleap available to any caller, place it in the public section of the class definition, and call it like this:

Date::isleap(y);
As a finishing touch, I will redefine the default constructor to initialize its object with the current date. The final class definition, implementation and sample program are in
Listing 12, Listing 13, and Listing 14 respectively.

Summary

In these last two installments I've tried to illustrate how C++ supports data abstraction — the creation of user-defined types. Constructors cause objects to be initialized automatically when you declare them. You can protect class members from inadvertent access by making them private. Overloading common operators makes your objects look and feel like built-in types — a plus for readability and maintenance.