Code Capsules

A C++ Date Class, Part 1

Chuck Allison


Last month's column introduced a function, date_interval, that calculates the number of years, months, and days between two arbitrary dates. This month's column presents a C++ solution to the same problem. The essence of this approach is to create a new data type, which behaves much the same as built-in types do. In other words, you shift from a function-based approach ("How do I want to do things?") to an object-based one ("What are the elements, the objects, of my problem?"). Using C++ effectively requires a different way of thinking about problem solving. To make that shift, it helps to know why C++ exists in the first place.

A Tale of Two Languages

C++ had its beginnings at AT&T in the early 1980's with Bjarne Stroustrup's "C with Classes". He was seeking a way to speed up simulations written in Simula-67. "Class" is the Simula term for a user-defined type, and being able to define objects that mirror reality is a key to good simulations. What better way to get fast simulations than to add classes to C, the fastest procedural language?

Choosing C not only provided an efficient vehicle for classes, but a portable one as well. Although other languages supported data abstraction through classes long before C++, it is now the most widespread. Almost every major platform that has a C compiler also supports C++. The last I heard, the C++ user base was doubling every seven months.

A first look at C++ can be overwhelming. If you're coming from C, you need to add the following (and then some) to your vocabulary:abstract class, access specifier, base class, catch clause, class, class scope, constructor, copy constructor, default argument, default constructor, delete operator, derived class, destructor, exception, exception handler, friend, inheritance, inline function, manipulator, member function, multiple inheritance, nested class, new handler, new operator, overloading, pointer to member, polymorphism, private, protected, public, pure virtual function, reference, static member, stream, template, this pointer, try block, type-safe linkage, virtual base class, virtual function.

The good news is that C++ is a powerful, efficient, object-oriented language able to handle complex applications. The bad news is that the language itself must therefore be somewhat complex, and is more difficult to master than C. And C itself is part of the problem. C++ is a hybrid, a blending of object-oriented features with a popular systems programming language. It is impossible to introduce such a rich set of new features without the host language having to bend a little. Yet compatibility with C is a major goal of the design of C++. As Bjarne stated in his keynote address to the ANSI C++ committee, C++ is an "engineering compromise," and must be kept "as close as possible to C, but no closer." How close is still being determined.

But there is more good news.

An Incremental Journey

You can use C++ effectively without having to master all of it. In fact, object-oriented technology promises that if vendors do their job (providing well-designed class libraries, crafted for reuse and extensibility), then your job of building applications will be easier. Current products, such as Borland's Application Frameworks, are proving this to be true in many cases.

If you feel you must master the language, you can do it in steps, and still be productive on the way. Three plateaus have emerged:

1. A Better C

2. Data Abstraction

3. Object-oriented programming

You can use C++ as a better C because it is safer and more expressive than C. Features on this plateau include type-safe linkage, mandatory function prototypes, inline functions, the const qualifier (yes, ANSI C borrowed it from C++), function overloading, default arguments, references, and direct language support for dynamic memory management. You will also need to be aware of the incompatibilities that exist between the two languages. There is a robust subset common to both which Plum and Saks call "Type-safe C" (see C++ Programming Guidelines, Plum and Saks, Plum-Hall, 1992).

As I will illustrate in this article and the next, C++ supports data abstraction — user-defined types that behave essentially as built-in types. The data abstraction mechanisms are classes, access specifiers, constructors and destructors, operator over-loading, templates, and exceptions.

Object-oriented programming takes data abstraction a step further by making the relationships between classes explicit. The two key concepts are inheritance (defining a new class by stating how it is the same and how it is different from another, reusing the sameness) and polymorphism (providing a single interface to a family of related operations, transparently resolved at runtime). C+ + supports inheritance and polymorphism through class derivation and virtual functions, respectively.

Classes

A class is just an extended struct. In addition to data members, you define member functions that act upon objects of the class. The definition of the Date class is in the file date.h in Listing 1. It differs from last month's C version because interval is a member function instead of a global function. The implementation of Date::interval() is in Listing 2. The double colon is called the scope resolution operator. It tells the compiler that interval is a member of the Date class. The ampersand in the prototype for Date::interval() indicates that its parameter is passed by reference. (See the sidebar on references.) The program in Listing 3 shows how to use the Date class. You must use structure member syntax to call Date:: interval():

   result = d1.interval (d2);
The structure tag Date serves as a type specifier, just like built-in types do (i.e., you can define a Date object without using the struct keyword). There is never a need to define

typedef struct Date Date;
In fact, the concept of class is so fundamental that C++ has merged the separate name space for structure tags that exists in C with that of ordinary identifiers.

Note that I have defined isleap as an inline function in Listing 2 (it was a macro in the C version). Inline functions get expanded "in-line" like macros do, but also perform all the scope and type checking that normal functions do. Unless you need to use the stringizing or token-pasting operations of the preprocessor, you don't need function-like macros in C++.

Now consider the statement

   years = d2.year - year;
in Listing 2. What object does year refer to? In the C version, this statement appeared as

   years = d2.year - d1.year;
Since a member function is always called in association with an object (e.g., d1. interval (d2)), then un-prefixed occurrences of data members naturally refer those of the associated object (in this case, year refers to d1.year). The this keyword represents a pointer to the underlying object, so I could make the more explicit statement:

   years = d2.year - this->year
but this is rarely done.

In Listing 4 I have added the following statements to the class definition:

   Date();
   Date(int,int,int);
These are special member functions called constructors. Constructors allow you to specify how to initialize an object when it is created. The first, called the default constructor (because it takes no arguments), is used when you define a date object without any initializers:

   Date d;
The following statement invokes the second:

   Date d(10,1,51);
When the implementation of member functions is trivial, you can make them inline by moving the implementation inside the class definition itself. (See Listing 7 — don't forget to mentally remove them from Listing 5. ) The test program in Listing 6 puts off constructing the objects d1, d2, and result until they are needed. (Object definitions can appear anywhere a statement can in C++.)

I have almost illustrated the key feature of data abstraction, namely encapsulation. Encapsulation occurs when a userdefined type has a well-defined boundary between its internal representation and its public interface. To truly say that I have created a new type that acts like a built-in type, I must disallow any inadvertent access to its internal representation. For example, at this point, the user could execute a statement such as

   d1.month = 20;
A well-behaved object controls the access to its data members. In a practical date class, I want to allow the user to query the month, day, and year, but not to set them directly, so I make them private, and provide accessor functions to retrieve their values (see Listing 8) . Since having private members is the more common situation, I usually replace the struct keyword with the keyword class, whose members are private by default (see Listing 9) . Accessor functions like get_month don't alter the private part of a Date object, so I declare them as const member functions. (Date::interval() is also const — don't forget to add const to its definition in the implementation file date3.cpp also.) I must now replace references to data members with calls to the accessor functions in tdate3.cpp (see Listing 10) .

We are only about half way to a complete C++-style approach to a date class. Next month we will incorporate stream I/O, static members, and operator overloading.