9. Technicalities: Classes, etc.

9.1 User-defined types

The C++ language provides you with some built-in types, such as char, int, and double. 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.

9.2 Classes and members

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 call var.mf(9), the m in the definition of mf() refers to var.m.

9.3 Interface and implementation

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

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)
                            

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.

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;              // add 1 to today
    tomorrow.d = today.d+1;            // use tomorrow
    cout << tomorrow << '\n';
}
                        

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.

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)
                    
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;               // (today.m==14 makes today invalid)
                        

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

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.

Defining member functions outside the class declaration is the preferred C++ style. It makes the declaration cleaner, and reduces recompilation. But for small functions, the benefits of inlining may help. 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;
}
                        
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, mcorrectly refers to d1.m and in the second call it refers to d2.m..

9.4.6 Reporting errors

What do we do when we find an invalid date? Where in the code do we look for invalid dates? 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';
dxy.add_day(2);
}
catch(Date::Invalid) {
    error("invalid date");
}
                        

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

Drill

Write the date class above, along with a main that has tests for valid and invalid dates. Include increment methods for year, month, and day. Write tests to make sure these do not give you dates like 14/34/2018!

9.5 Enumerations

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
};
                    

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
};
                    
                    
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)
                    

“plain” enums are less strict than enum classes.

Drill

Add enums for month and day of the week to your Date class. Add a vector of strings that maps months from the enum to words for the months. Add a method print_long_date that prints out the month as a word not a number. Think about how to add the option to print day of the week.

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”)
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++:

  • Keep interfaces complete
  • Keep interfaces minimal
  • Provide constructors
  • Support copying (or prohibit it)
  • Use types to provide good argument checking
  • Identify nonmodifying member functions
  • Free all resources in the destructor
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);
// . . .
private:
    int y;
    Month m;
    int d;
};
                        
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;
                        
9.7.3 Default constructors

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

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.

9.7.5 Members and "helper functions"

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.

Helper functions are also called convenience functions, auxiliary 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.

Test Yourself!
  1. What are the two parts of a class?
    1. variants and constants
    2. interface and implementation
    3. local and global
    4. interference and instantiation
  2. Which of the following is a class invariant for Date?
    1. we must always have a month less than 13
    2. month should be defined as an int
    3. printed dates should never take up more than one column in a spreadsheet
    4. the year cannot be a leap year
  3. When should the body of a function (its definition) be put in the class definition?
    1. when the function returns a built-in type
    2. when the function is only a line or two
    3. when the function is a very important part of the class
    4. when the function returns a user-defined type
  4. What would be a reasonable way to overload the plus (+) operator?
    1. to perform a hash function on some input string
    2. to access a website and add all of its pages to your site
    3. to add two complex numbers
  5. What does adding const to a member function do?
    1. promises that the function will not change the object
    2. says the function is a constructor
    3. asserts that the function will constantly return the same value for the same input
  6. A class can hold the following
    1. data
    2. functions
    3. both data & functions
    4. none of the mentioned
  7. What defines the member of the class externally?
    1. ~
    2. ::
    3. none of the mentioned
    4. :
  8. Which of the following is an access specifier?
    1. All specified
    2. protected
    3. private
    4. public
  9. Which of the following access specifiers can be used for an interface?
    1. public
    2. All specified
    3. private
    4. protected
  10. Constructors are used to
    1. initialize the objects
    2. construct the data members
    3. both initialize the objects & construct the data members
    4. none of the mentioned
  11. Which of the following is the correct way of implementing an interface A by Class B?
    1. class B extends A{}
    2. class B imports A{}
    3. all specified
    4. class B implements A{}
  12. Member function of a class can ..
    1. Access subclass members
    2. Access all the members of the class
    3. Access only the private members of the class
    4. Access only Public members of the class
  13. Which of the following is not necessary for constructors?
    1. It must contain a definition body
    2. Its name must be same as that of class
    3. It must not have any return type
    4. It can contains arguments
  14. Which of the following operators cannot be overloaded?
    1. All mentioned
    2. . (Dot operator)
    3. ?: (Ternary Operator)
    4. :: (Scope resolution operator)
  15. Special data types defined by users is called
    1. Conditional type
    2. Compound type
    3. none specified
    4. Enumeration type
  16. In C++, const qualifier can be applied to : 1. Member functions of a class; 2. Function arguments; 3. to a class data member which is declared as static; 4. Reference variables
    1. all
    2. only 1, 3 and 4
    3. only 1, 2 and 3
    4. only 1,2 and 4
Answers

1. b; 2. a; 3. b; 4. c; 5. a; 6. c; 7. b; 8. a; 9. a; 10. a; 11. d; 12. b; 13. a; 14. a; 15. d; 16. a;

Drill

Code a full date class and write a main showing its complete usage. You should include:

  1. Exceptions for bad dates.
  2. Methods to get and to increment the day, month and year.
  3. A month enum class to ensure argument order.
  4. A boolean test for leapyear outside the class.
  5. Operator overloading for <<, >>, ==, and !=.
  6. Methods for day of the week, the next weekday, and a function outside the class for testing date validity, that will be passed y, m and d.