Time
Class Case Study: Constructors with Default ArgumentsThe program of Figs. 9.5–9.7 enhances class Time
to demonstrate how arguments can be passed to a constructor implicitly.
Like other functions, constructors can specify default arguments. Line 13 of Fig. 9.5 declares a Time
constructor with default arguments, specifying a default value of zero for each argument passed to the constructor. The constructor is declared explicit
because it can be called with one argument. We discuss explicit
constructors in detail in Section 10.13.
In Fig. 9.6, lines 11–13 define the Time
constructor that receives values for parameters hour
, minute
and second
that will be used to initialize private
data members hour
, minute
and second
, respectively. A constructor that defaults all its arguments is also a default constructor—that is, a constructor that can be invoked with no arguments. There can be at most one default constructor per class. The version of class Time
in this example provides set and get functions for each data member. The Time
constructor now calls setTime
, which calls the setHour
, setMinute
and setSecond
functions to validate and assign values to the data members.
Any change to the default argument values of a function requires the client code to be recompiled (to ensure that the program still functions correctly).
In Fig. 9.6, line 12 of the constructor calls member function setTime
with the values passed to the constructor (or the default values). Function setTime
calls setHour
to ensure that the value supplied for hour
is in the range 0–23, then calls setMinute
and setSecond
to ensure that the values for minute
and second
are each in the range 0–59. Functions setHour
(lines 23–30), setMinute
(lines 33–40) and setSecond
(lines 43–50) each throw an exception if an out-of-range argument is received.
Function main
in Fig. 9.7 initializes five Time
objects—one with all three arguments defaulted in the implicit constructor call (line 15), one with one argument specified (line 16), one with two arguments specified (line 17), one with three arguments specified (line 18) and one with three invalid arguments specified (line 28). Each explicit constructor call (lines 16–18 and 28) uses C++11 list-initializer syntax. The program displays each object in universal-time and standard-time formats. For Time
object t5
(line 28), the program displays an error message because the constructor arguments are out of range.
Time
’s Set and Get Functions and ConstructorTime
’s set and get functions are called throughout the class’s body. In particular, function setTime
(lines 16–20 of Fig. 9.6) calls functions setHour
, setMinute
and setSecond
, and functions toUniversalString
and toStandardString
call functions getHour
, getMinute
and getSecond
in lines 64–65 and lines 72–74.
In each case, these functions could have accessed the class’s private
data directly. However, consider changing the representation of the time from three int
values (requiring 12 bytes of memory on systems with four-byte int
s) to a single int
value representing the total number of seconds that have elapsed since midnight (requiring only four bytes of memory). If we made such a change, only the bodies of the functions that access the private
data directly would need to change—in particular, the individual set and get functions for the hour
, minute
and second
. There would be no need to modify the bodies of functions setTime
, toUniversalString
or toStandardString
, because they do not access the data directly.
Similarly, the Time
constructor could be written to include a copy of the appropriate statements from function setTime
. Doing so may be slightly more efficient, because the extra call to setTime
is eliminated. However, duplicating statements in multiple functions or constructors makes changing the class’s internal data representation more difficult. Having the Time
constructor call setTime
and having setTime
call setHour
, setMinute
and setSecond
enables us to limit the changes to code that validates the hour
, minute
or second
to the corresponding set function. This reduces the likelihood of errors when altering the class’s implementation.
If a member function of a class already provides all or part of the functionality required by a constructor or other member functions of the class, call that member function from the constructor or other member functions. This simplifies the maintenance of the code and reduces the likelihood of an error if the code implementation is modified. As a general rule: Avoid repeating code.
A constructor can call other member functions of the class, such as set or get functions, but because the constructor is initializing the object, the data members may not yet be initialized. Using data members before they have been properly initialized can cause logic errors.
Making data members private
and controlling access, especially write access, to those data members through public
member functions helps ensure data integrity.
The benefits of data integrity are not automatic simply because data members are made private
—you must provide appropriate validity checking.
Section 6.16 showed how to overload functions. A class’s constructors and member functions can also be overloaded. Overloaded constructors typically allow objects to be initialized with different types and/or numbers of arguments. To overload a constructor, provide in the class definition a prototype for each version of the constructor, and provide a separate constructor definition for each overloaded version. This also applies to the class’s member functions.
In Figs. 9.5–9.7, the Time
constructor with three parameters had a default argument for each parameter. We could have defined that constructor instead as four overloaded constructors with the following prototypes:
Time(); // default hour, minute and second to 0
explicit Time(int); // init hour; default minute and second to 0
Time(int, int); // initialize hour and minute; default second to 0
Time(int, int, int); // initialize hour, minute and second
Just as a constructor can call a class’s other member functions to perform tasks, C++11 allows constructors to call other constructors in the same class. The calling constructor is known as a delegating constructor—it delegates its work to another constructor. This is useful when overloaded constructors have common code that previously would have been defined in a private
utility function and called by all the constructors.
The first three of the four Time
constructors declared above can delegate work to one with three int
arguments, passing 0 as the default value for the extra parameters. To do so, you use a member initializer with the name of the class as follows:
Time::Time() : Time{0, 0, 0} {} // delegate to Time(int, int, int)
// delegate to Time(int, int, int)
Time::Time(int hour) : Time{hour, 0, 0} {}
// delegate to Time(int, int, int)
Time::Time(int hour, int minute) : Time{hour, minute, 0} {}