Chapter 9. Custom Classes

Images

Objectives

In this chapter, you’ll:

Define a custom class and use it to create objects.

Implement a class’s behaviors as member functions and attributes as data members.

Access and manipulate private data members through public get and set functions to enforce data encapsulation.

Use a constructor to initialize an object’s data.

Separate a class’s interface from its implementation for reuse.

Access class members via the dot (.) and arrow (->) operators.

Use destructors to perform “termination housekeeping.”

Learn why returning a reference or a pointer to private data is dangerous.

Assign the data members of one object to those of another.

Create objects composed of other objects.

Use friend functions and declare friend classes.

Access non-static class members via the this pointer.

Use static data members and member functions.

Use structs to create aggregate types, and use C++20 designated initializers to initialize aggregate members.

Do an objects-natural case study that serializes objects using JavaScript Object Notation (JSON) and the cereal library.

Outline

9.1 Introduction

9.2 Test-Driving an Account Object

9.3 Account Class with a Data Member and Set and Get Member Functions

9.3.1 Class Definition

9.3.2 Access Specifiers private and public

9.4 Account Class: Custom Constructors

9.5 Software Engineering with Set and Get Member Functions

9.6 Account Class with a Balance

9.7 Time Class Case Study: Separating Interface from Implementation

9.7.1 Interface of a Class

9.7.2 Separating the Interface from the Implementation

9.7.3 Class Definition

9.7.4 Member Functions

9.7.5 Including the Class Header in the Source-Code File

9.7.6 Scope Resolution Operator (::)

9.7.7 Member Function setTime and Throwing Exceptions

9.7.8 Member Function to24HourString

9.7.9 Member Function to12HourString

9.7.10 Implicitly Inlining Member Functions

9.7.11 Member Functions vs. Global Functions

9.7.12 Using Class Time

9.7.13 Object Size

9.8 Compilation and Linking Process

9.9 Class Scope and Accessing Class Members

9.10 Access Functions and Utility Functions

9.11 Time Class Case Study: Constructors with Default Arguments

9.11.1 Class Time

9.11.2 Overloaded Constructors and C++11 Delegating Constructors

9.12 Destructors

9.13 When Constructors and Destructors Are Called

9.14 Time Class Case Study: A Subtle Trap—Returning a Reference or a Pointer to a private Data Member

9.15 Default Assignment Operator

9.16 const Objects and const Member Functions

9.17 Composition: Objects as Members of Classes

9.18 friend Functions and friend Classes

9.19 The this Pointer

9.19.1 Implicitly and Explicitly Using the this Pointer to Access an Object’s Data Members

9.19.2 Using the this Pointer to Enable Cascaded Function Calls

9.20 static Class Members—Classwide Data and Member Functions

9.21 Aggregates in C++20

9.21.1 Initializing an Aggregate

9.21.2 C++20: Designated Initializers

9.22 Objects Natural Case Study: Serialization with JSON

9.22.1 Serializing a vector of Objects containing public Data

9.22.2 Serializing a vector of Objects containing private Data

9.23 Wrap-Up

9.1 Introduction1

Section 1.16 presented a friendly introduction to object orientation, discussing classes, objects, data members (attributes) and member functions (behaviors). In our objects-natural case studies, you’ve created objects of existing classes and called their member functions to make the objects perform powerful tasks without having to know how these classes worked internally.

1. This chapter depends on the terminology and concepts introduced in Section 1.16, Introduction to Object Orientation and “Objects Natural.”

This chapter begins our deeper treatment of object-oriented programming as we craft valuable custom classes. C++ is an extensible programming language—each class you create becomes a new type you can use to create objects. Some development teams in industry work on applications that contain hundreds, or even thousands, of custom classes.

9.2 Test-Driving an Account Object

We begin our introduction to custom classes with three examples that create an Account class representing a simple bank account. First, let’s look at the main program and output, so you can see an object of our initial Account class in action. To help you prepare for the larger programs you’ll encounter later in this book and in industry, we define the Account class and main in separate files—main in AccountTest.cpp (Fig. 9.1) and class Account in Account.h, which we’ll show in Fig. 9.2.

 1   // Fig. 9.1: AccountTest.cpp
 2   // Creating and manipulating an Account object.
 3   #include <iostream>
 4   #include <string>
 5   #include <fmt/format.h> // In C++20, this will be #include <format>
 6   #include "Account.h"
 7
 8   using namespace std;
 9
10   int main() {
11      Account myAccount{}; // create Account object myAccount
12
13      // show that the initial value of myAccount's name is the empty string
14      cout << fmt::format("Initial account name: {}
", myAccount.getName());
15
16      // prompt for and read the name
17      cout << "Enter the account name: ";
18      string name{};
19      getline(cin, name); // read a line of text
20      myAccount.setName(name); // put name in the myAccount object
21
22      // display the name stored in object myAccount
23      cout << fmt::format("Updated account name: {}
", myAccount.getName());
24   }
Initial account name:
Enter the account name: Jane Green
Updated account name: Jane Green

Fig. 9.1 Creating and manipulating an Account object.

 1   // Fig. 9.2: Account.h
 2   // Account class with a data member and
 3   // member functions to set and get its value.
 4   #include <string>
 5   #include <string_view>
 6
 7   class Account {
 8   public:
 9      // member function that sets m_name in the object
10      void setName(std::string_view name) {
11         m_name = name; // replace m_name's value with name
12      }
13
14      // member function that retrieves the account name from the object
15      const std::string& getName() const {
16         return m_name; // return m_name's value to this function's caller
17      }
18   private:
19      std::string m_name; // data member containing account holder's name
20   }; // end class Account

Fig. 9.2 Account class with a data member and member functions to set and get its value.

Instantiating an Object

Typically, you cannot call a class’s member functions until you create an object of that class.2 Line 11

Account myAccount{}; // create Account object myAccount

2. In Section 9.20, you’ll see that static member functions are an exception.

creates an object called myAccount. The variable’s type is Account—the class we’ll define in Fig. 9.2.

Headers and Source-Code Files

When we declare int variables, the compiler knows what int is—it’s a fundamental type that’s built into C++. In line 11, however, the compiler does not know in advance what type Account is—it’s a user-defined type.

When packaged properly, new classes can be reused by other programmers. It’s customary to place a reusable class definition in a file known as a header with a .h filename extension.3 You include that header wherever you need to use the class, as you’ve been doing throughout this book with C++ Standard Library and third-party library classes.

3. C++ Standard Library headers, like <iostream>, do not use the .h filename extension and some C++ programmers prefer the .hpp extension.

We tell the compiler what an Account is by including its header, as in line 6:

#include "Account.h"

If we omit this, the compiler issues error messages wherever we use class Account and any of its capabilities. A header that you define in your program is placed in double quotes (""), rather than the angle brackets (<>). The double quotes tell the compiler to check the folder containing AccountTest.cpp (Fig. 9.1) before the compiler’s header search path.

Calling Class Account’s getName Member Function

Class Account’s getName member function returns the name stored in a particular Account object. Line 14 calls myAccount.getName() to get the myAccount object’s initial name, which is the empty string. We’ll say more about this shortly.

Calling Class Account’s setName Member Function

The setName member function stores a name in a particular Account object. Line 20 calls

myAccount’s setName member function to store name’s value in the object myAccount.

Displaying the Name That Was Entered by the User

To confirm that myAccount now contains the name you entered, line 23 calls member function getName again and displays its result.

9.3 Account Class with a Data Member and Set and Get Member Functions

Now that we’ve seen class Account in action (Fig. 9.1), we present its internal details.

9.3.1 Class Definition

Class Account (Fig. 9.2) contains an m_name data member (line 19) that stores the account holder’s name. Each object of the class has its own copy of the class’s data members.4 Later, we’ll add a balance data member to keep track of the money in each Account. Class Account also contains:

• member function setName (lines 10–12) that stores a name in an Account, and

• member function getName (lines 15–17) that retrieves a name from an Account.

4. In Section 9.20, you’ll see that static data members are an exception.

Keyword class and the Class Body

The class definition begins with the keyword class (line 7) followed immediately by the class’s name—in this case, Account. By convention:

• each word in a class name starts with a capital first letter, and

• data-member and member-function names begin with a lowercase first letter.

Every class’s body is enclosed in braces {} (lines 7 and 20). The class definition terminates with a required semicolon (line 20).

Data Member m_name of Type std::string

Recall from Section 1.16 that an object has attributes, implemented as data members. Each object maintains its own copy of these throughout its lifetime. Usually, a class also contains member functions that manipulate the data members of particular objects of the class. The data members exist:

• before a program calls member functions on a specific object,

• while the member functions are executing and

• after the member functions finish executing.

Data members are declared inside a class definition but outside the class’s member functions. Line 19

std::string m_name; // data member containing account holder's name

declares a string data member called m_name. The "m_" prefix is a common naming convention to indicate that a variable represents a data member. If there are many Account objects, each has its own m_name. Because m_name is a data member, it can be manipulated by the class’s member functions. Recall that the default string value is the empty string (""), which is why line 14 in main (Fig. 9.1) did not display a name.

By convention, C++ programmers typically place a class’s data members last in the class’s body. You can declare the data members anywhere in the class outside its member-function definitions, but scattering the data members can lead to hard-to-read code.

Use std:: with Standard Library Components in Headers

Throughout the Account.h header (Fig. 9.2), we use std:: when referring to string (lines 10, 15 and 19). For subtle reasons that we explain in Section 19.4, headers should not contain using directives or using declarations.

setName Member Function

Member function setName (lines 10–12):

void setName(std::string_view name) {
   m_name = name; // replace m_name's value with name
}

Images SE receives a string_view representing the Account’s name and assigns the name argument to data member m_name. Recall that a string_view is a read-only view into a character sequence, such as a std::string or a C-string. This copies name’s characters into m_name. The "m_" in m_name makes it easy to distinguish the parameter name from the data member m_name.

getName Member Function

Member function getName (lines 15–17):

const std::string& getName() const {
   return m_name; // return m_name's value to this function's caller
   }

has no parameters and returns a particular Account object’s m_name to the caller as a const std::string&. Declaring the returned reference const ensures that the caller cannot modify the object’s data via that reference.

const Member Functions

Note the const to the right of the parameter list in getName’s header (line 15). When returning m_name, member function getName does not, and should not, modify the Account object on which it’s called. Declaring a member function const tells the compiler, “this function should not modify the object on which it’s called—if it does, please issue a compilation error.” This can help you locate errors if you accidentally insert code that would modify the object. It also tells the compiler that getName may be called on a const Account object, or called via a reference or pointer to a const Account.

9.3.2 Access Specifiers private and public

Images CG The keyword private (line 18) is an access specifier. Each access specifier is followed by a required colon (:). Data member m_name’s declaration (line 19) appears after private: to indicate that m_name is accessible only to class Account’s member functions.5 This is known as information hiding (or, more generally, as hiding implementation details) and is a recommended practice of the C++ Core Guidelines6 and object-oriented programming in general. The data member m_name is encapsulated (hidden) and can be used only in class Account’s setName and getName member functions. Most data-member declarations appear after the private: access specifier. We generally omit the colon when we refer to the private and public access specifiers in the text, as we did in this sentence.

5. Or to “friends” of the class, as you’ll see in Section 9.18.

6. C++ Core Guidelines. Accessed July 12, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-private.

This class also contains the public access specifier (line 8):

public:

Images SE Data members or member functions listed after access specifier public—and before the next access specifier if there is one—are “available to the public.” They can be used anywhere an object of the class is in scope. Making a class’s data members private facilitates debugging because problems with data manipulations are localized to the class’s member functions. In Chapter 10, we’ll introduce the protected access specifier.

Default Access for Class Members

By default, everything in a class is private, unless you specify otherwise. Once you list an access specifier, everything has that level of access until you list another access specifier. The access specifiers public and private may be repeated, but this is unnecessary and can be confusing. We prefer to list public only once, grouping everything that’s public. We prefer to list private only once, grouping everything that’s private.

9.4 Account Class: Custom Constructors

As you saw in the preceding example, when an Account object is created, its string data member m_name is initialized to the empty string by default. But what if you want to provide a name when you first create an Account object? Each class can define constructors that specify custom initialization for objects of that class. A constructor is a special member function that must have the same name as the class. Constructors cannot return values, so they do not specify a return type (not even void). C++ guarantees that a constructor is called when each object is created, so this is the ideal point to initialize an object’s data members. Every time you created an object so far in this book, the corresponding class’s constructor was called to initialize the object. As you’ll soon see, classes may have overloaded constructors.

Like member functions, constructors can have parameters—the corresponding argument values help initialize the object’s data members. For example, you can specify an Account object’s name when the object is created, as you’ll do in line 12 of Fig. 9.4:

Account account1{"Jane Green"};

In the preceding statement, the string "Jane Green" is passed to the Account class’s constructor and used to initialize the account1 object’s data. The preceding statement assumes that class Account has a constructor that can receive a string argument.

Account Class Definition

Figure 9.3 shows class Account with a constructor that receives an accountName parameter and uses it to initialize the data member m_name when an Account object is created.

 1   // Fig. 9.3: Account.h
 2   // Account class with a constructor that initializes the account name.
 3   #include <string>
 4   #include <string_view>
 5
 6   class Account {
 7   public:
 8      // constructor initializes data member m_name with the parameter name
 9      explicit Account(std::string_view name)
10         : m_name{name} { // member initializer
11        // empty body
12      }
13
14      // function to set the account name
15      void setName(std::string_view name) {
16         m_name = name; // replace m_name's value with name
17      }
18
19      // function to retrieve the account name
20      const std::string& getName() const {
21         return m_name;
22      }
23   private:
24      std::string m_name; // account name data member
25   }; // end class Account

Fig. 9.3 Account class with a constructor that initializes the account name.

Account Class’s Custom Constructor Definition

Lines 9–12 of Fig. 9.3 define Account’s constructor:

explicit Account(std::string_view name)
  : m_name{name} { // member initializer
  // empty body }

Usually, constructors are public, so any code with access to the class definition can create and initialize objects of that class.7 Line 9 indicates that the constructor has a name parameter of type string_view. When you create a new Account object, you must pass a person’s name to the constructor, which then initializes the data member m_name with the contents of the string_view parameter.

7. Section 11.10.2 discusses why you might use a private constructor.

The constructor’s member-initializer list (line 10):

: m_name{name}

initializes the m_name data member. Member initializers appear between a constructor’s parameter list and the left brace that begins the constructor’s body. You separate the member initializer list from the parameter list with a colon (:).

Images SE Each member initializer consists of a data member’s variable name followed by braces containing its initial value.8 This member initializer calls the std::string class’s constructor that receives a string_view. If a class has more than one data member, each member initializer is separated from the next by a comma. Member initializers execute in the order you declare the data members in the class so, for clarity, list the member initializers in the same order. The member initializer list executes before the constructor’s body executes.

8. Occasionally, parentheses rather than braces may be required, such as when initializing a vector of a specified size, as we did in Fig. 6.14.

Images CG Images PERF Though you can perform initialization with assignment statements in the constructor’s body, the C++ Core Guidelines recommend using member initializers.9 You’ll see later that member initializers can be more efficient. Also, you’ll see that certain data members must be initialized using the member-initializer syntax, because you cannot assign to them in the constructor’s body.

9. C++ Core Guidelines. Accessed July 12, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-initialize.

explicit Keyword

Images CG A constructor with only one parameter should be declared explicit to prevent the compiler from using the constructor to perform implicit type conversions.10 The keyword explicit means that Account’s constructor must be called explicitly, as in:

Account account1{"Jane Green"};

10. C++ Core Guidelines. Accessed July 12, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-explicit.

Images ERR For now, simply declare all single-parameter constructors explicit. In Section 11.13, you’ll see that single-parameter constructors without explicit can be called implicitly to perform type conversions. Such implicit constructor calls can lead to subtle errors and are generally discouraged. Line 9 of Fig. 9.3 does not specify a return type (not even void) because, again, constructors cannot return values. Also, constructors cannot be declared const (simply because initializing an object modifies it).

Initializing Account Objects When They’re Created

The AccountTest program (Fig. 9.4) initializes two Account objects using the constructor. Line 12 creates the Account object account1:

Account account1{"Jane Green"};
 1   // Fig. 9.4: AccountTest.cpp
 2   // Using the Account constructor to initialize the m_name data
 3   // member at the time each Account object is created.
 4   #include <iostream>
 5   #include <fmt/format.h> // In C++20, this will be #include <format>
 6   #include "Account.h"
 7
 8   using namespace std;
 9
10   int main() {
11      // create two Account objects
12      Account account1{"Jane Green"};
13      Account account2{"John Blue"};
14
15      // display initial each Account's corresponding name
16      cout << fmt::format("account1 name is: {}
account2 name is: {}
",
17                 account1.getName(), account2.getName());
18   }
account1 name is: Jane Green
account2 name is: John Blue

Fig. 9.4 Using the Account constructor to initialize the m_name data member at the time each Account object is created.

When you create an object, C++ calls the class’s constructor to initialize that object. In line 12, the argument "Jane Green" is used by the constructor to initialize the new object’s m_name data member, as specified in lines 9–12 of Fig. 9.3. Line 13 of Fig. 9.4 repeats this process, passing the argument "John Blue" to initialize m_name for account2:

Account account2{"John Blue"};

To confirm the objects were initialized properly, lines 16–17 call the getName member function to get each object’s name. The output shows different names, confirming that each Account maintains its own copy of the class’s data member m_name.

Default Constructor

Recall that line 11 of Fig. 9.1 created an Account object with empty braces to the right of the object’s variable name:

Account myAccount{};

which, for an object of a class, is equivalent to:

Account myAccount;

In the preceding statements, C++ implicitly calls Account’s default constructor. In any class that does not define a constructor, the compiler generates a default constructor with no parameters. The default constructor does not initialize the class’s fundamental-type data members but does call the default constructor for each data member that’s an object of another class. For example, though you do not see this in the first Account class’s code (Fig. 9.2), Account’s default constructor calls class std::string’s default constructor to initialize the data member m_name to the empty string (""). An uninitialized fundamental-type variable contains an undefined (“garbage”) value.

There’s No Default Constructor in a Class That Defines a Constructor

Images SE If you define a custom constructor for a class, the compiler will not create a default constructor for that class. In that case, you will not be able to create an Account object by calling the constructor with no arguments unless the custom constructor you define has an empty parameter list or has default arguments for all its parameters. We’ll show later that you can force the compiler to create the default constructor even if you’ve defined non-default constructors. Unless default initialization of your class’s data members is acceptable, provide a custom constructor that initializes each new object’s data members with meaningful values.

C++’s Special Member Functions

In addition to the default constructor, the compiler can generate default versions of five other special member functions—a copy constructor, a move constructor, a copy assignment operator, a move assignment operator and a destructor. We’ll briefly introduce copy construction, copy assignment and destructors in this chapter. In Chapter 11, we’ll discuss the details of all six special member functions, including:

• when they’re auto-generated,

• when you might need to define custom versions of each, and

• the various C++ Core Guidelines for these special member functions.

You’ll see that you should try to construct your custom classes such that the compiler can auto-generate these special member functions for you—this is called the “Rule of Zero.”

9.5 Software Engineering with Set and Get Member Functions

As you’ll see in the next section, set and get member functions can validate attempts to modify private data and control how that data is presented to the caller, respectively. These are compelling software engineering benefits.

A client of the class is any other code that calls the class’s member functions. If a data member were public, any client could see the data and do whatever it wanted with it, including setting it to an invalid value.

You might think that even though a client cannot directly access a private data member, the client can nevertheless do whatever it wants with the variable through public set and get functions. You’d think that you could peek at the private data (and see exactly how it’s stored in the object) anytime with the public get function and that you could modify the private data at will through the public set function.

Images SE Actually, set functions can be written to validate their arguments and reject any attempts to set the data to incorrect values, such as

• a negative body temperature

• a day in March outside the range 1 through 31, or

• a product code not in the company’s product catalog.

Images SE Images SE A get function can present the data in a different form, keeping the object’s actual data representation hidden from the user. For example, a Grade class might store a numeric grade as an int between 0 and 100, but a getGrade member function might return a letter grade as a string, such as "A" for grades between 90 and 100, "B" for grades between 80 and 89, etc. Tightly controlling the access to and presentation of private data can reduce errors while increasing your programs’ robustness, security and usability.

Conceptual View of an Account Object with Encapsulated Data

You can think of an Account object, as shown in the following diagram. The private data member m_name, represented by the inner circle, is hidden inside the object and accessible only via an outer layer of public member functions, represented by the outer ring containing getName and setName. Any client that needs to interact with the Account object can do so only by calling the public member functions of the outer layer.

Images

Images SE Generally, data members are private, and the member functions that a class’s clients need to use are public. Later, we’ll discuss why you might use a public data member or a private member function. Using public set and get functions to control access to private data makes programs clearer and easier to maintain. Change is the rule rather than the exception. You should anticipate that your code will be modified, and possibly often.

9.6 Account Class with a Balance

Images CG Figure 9.5 defines an Account class that maintains two related pieces of data—a bank account’s balance and the account holder’s name. The C++ Core Guidelines recommend defining related data items in a class (or, as you’ll see in Section 9.21, a struct).11

 1   // Fig. 9.5: Account.h
 2   // Account class with m_name and m_balance data members, and a
 3   // constructor and deposit function that each perform validation.
 4   #include <string>
 5   #include <string_view>
 6
 7   class Account {
 8   public:
 9      // Account constructor with two parameters
10      Account(std::string_view name, double balance)
11         : m_name{name} { // member initializer for m_name
12
13         // validate that balance is greater than 0.0; if not,
14         // data member m_balance keeps its default initial value of 0.0
15         if (balance > 0.0) { // if the balance is valid
16            m_balance = balance; // assign it to data member m_balance
17         }
18      }
19
20      // function that deposits (adds) only a valid amount to the balance
21      void deposit(double amount) {
22         if (amount > 0.0) { // if the amount is valid
23            m_balance += amount; // add it to m_balance
24         }
25      }
26
27      // function returns the account balance
28      double getBalance() const {
29         return m_balance;
30      }
31
32      // function that sets the account name
33      void setName(std::string_view name) {
34         m_name = name; // replace m_name's value with name
35      }
36
37      // function that returns the account name
38      const std::string& getName() const {
39         return m_name;
40      }
41   private:
42      std::string m_name; // account name data member
43      double m_balance{0.0}; // data member with default initial value
44   }; // end class Account

Fig. 9.5 Account class with m_name and m_balance data members, and a constructor and deposit function that each perform validation.

11. C++ Core Guidelines. Accessed July 12, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-org.

Data Member balance

A typical bank services many accounts, each with its own balance. Every object of this updated Account class contains its own copies of data members m_name and m_balance. Line 43 declares a double data member m_balance and initializes its value to 0.0:

double m_balance{0.0}; // data member with default initial value

Images CG Images CG Images PERF This is an in-class initializer. The C++ Core Guidelines recommend using in-class initializers when a data member should be initialized with a constant.12 The Core Guidelines also recommend when possible that you initialize all your data members with in-class initializers and let the compiler generate a default constructor for your class. A compiler-generated default constructor can be more efficient than one you define.13 Like m_name, we can use m_balance in the class’s member functions (lines 16, 23 and 29) because it’s a data member of the class.

12. C++ Core Guidelines. Accessed July 12, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-in-class-initializer.

13. C++ Core Guidelines. Accessed July 12, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-default.

Two-Parameter Constructor

The class has a constructor and four member functions. It’s common for someone opening an account to deposit money immediately, so the constructor (lines 10–18) receives a second parameter balance of type double that represents the starting balance. We did not declare this constructor explicit, because it cannot be called with only one parameter.

Lines 15–17 ensure that data member m_balance is assigned the balance parameter’s value only if that value is greater than 0.0:

if (balance > 0.0) { // if the balance is valid
   m_balance = balance; // assign it to data member m_balance
}

Otherwise, m_balance will still have its default initial value 0.0 that was set at line 43 in class Account’s definition.

deposit Member Function

Member function deposit (lines 21–25) receives a double parameter amount and does not return a value. Lines 22–24 ensure that parameter amount’s value is added to m_balance only if amount is greater than zero (that is, it’s a valid deposit amount).

getBalance Member Function

Member function getBalance (lines 28–30) allows the class’s clients to obtain a particular Account object’s m_balance value. The member function specifies return type double and an empty parameter list. Like member function getName, getBalance is declared const because, in the process of returning m_balance, the function does not, and should not, modify the Account object on which it’s called.

Manipulating Account Objects with Balances

The main function in Fig. 9.6 creates two Account objects (lines 10–11) and attempts to initialize them with a valid balance of 50.00 and an invalid balance of -7.00, respectively. For our examples, we assume that balances must be greater than or equal to zero. Lines 14–17 output both Accounts’ names and balances by calling each objects’ getName and getBalance member functions. The account2 object’s balance is initially 0.0 because the constructor rejected the attempt to start account2 with a negative balance, so account2’s m_balance data member retains its default initial value.

 1   // Fig. 9.6: AccountTest.cpp
 2   // Displaying and updating Account balances.
 3   #include <iostream>
 4   #include <fmt/format.h> // In C++20, this will be #include <format>
 5   #include "Account.h"
 6
 7   using namespace std;
 8
 9   int main() {
10      Account account1{"Jane Green", 50.00};
11      Account account2{"John Blue", -7.00};
12
13      // display initial balance of each object
14      cout << fmt::format("account1: {} balance is ${:.2f}
",
15                 account1.getName(), account1.getBalance());
16      cout << fmt::format("account2: {} balance is ${:.2f}

",
17                 account2.getName(), account2.getBalance());
18
account1: Jane Green balance is $50.00
account2: John Blue balance is $0.00

Fig. 9.6 Displaying and updating Account balances.

Reading a Deposit Amount from the User and Making a Deposit

Lines 19–22 prompt for, input and display the account1 deposit amount. Line 23 calls object account1’s deposit member function with variable amount as the argument to add that value to account1’s balance. Lines 26–29 (Fig. 9.6) output both Accounts’ names and balances again to show that only account1’s balance has changed.

19      cout << "Enter deposit amount for account1: "; // prompt
20      double amount;
21      cin >> amount; // obtain user input
22      cout << fmt::format("adding ${:.2f} to account1 balance

", amount);
23      account1.deposit(amount); // add to account1's balance
24
25      // display balances
26      cout << fmt::format("account1: {} balance is ${:.2f}
",
27                 account1.getName(), account1.getBalance());
28      cout << fmt::format("account2: {} balance is ${:.2f}

",
29                 account2.getName(), account2.getBalance());
30
Enter deposit amount for account1: 25.37
adding $25.37 to account1 balance

account1: Jane Green balance is $75.37
account2: John Blue balance is $0.00

Lines 31–33 prompt for, input and display account2’s deposit amount. Line 34 calls object account2’s deposit member function with variable amount as the argument to add that value to account2’s balance. Finally, lines 37–40 output both Accounts’ names and balances again to show that only account2’s balance has changed.

31      cout << "Enter deposit amount for account2: "; // prompt
32      cin >> amount; // obtain user input
33      cout << fmt::format("adding ${:.2f} to account2 balance

", amount);
34      account2.deposit(amount); // add to account2 balance
35
36      // display balances
37      cout << fmt::format("account1: {} balance is ${:.2f}
",
38                 account1.getName(), account1.getBalance());
39      cout << fmt::format("account2: {} balance is ${:.2f}
",
40                 account2.getName(), account2.getBalance());
41   }
Enter deposit amount for account2: 123.45
adding $123.45 to account2 balance

account1: Jane Green balance is $75.37
account2: John Blue balance is $123.45

9.7 Time Class Case Study: Separating Interface from Implementation

Each of our prior custom class definitions placed a class in a header for reuse, then included the header into a source-code file containing main, so we could create and manipulate objects of the class. Unfortunately, placing a complete class definition in a header reveals the class’s entire implementation to its clients. A header is simply a text file that anyone can open and read.

Images SE Conventional software-engineering wisdom says that to use an object of a class, the client code (e.g., main) needs to know only

• what member functions to call,

• what arguments to provide to each member function and

• what return type to expect from each member function.

The client code does not need to know how those functions are implemented. This is another example of the principle of least privilege.

If the client-code programmer knows how a class is implemented, the programmer might write client code based on the class’s implementation details. Ideally, if that implementation changes, the class’s clients should not have to change. Hiding the class’s implementation details makes it easier to change the implementation while minimizing, and hopefully eliminating, changes to client code.

Our next example creates and manipulates an object of class Time.14 We demonstrate two important C++ software engineering concepts:

Separating interface from implementation.

• Using the preprocessor directive “#pragma once” in a header to prevent the header code from being included into the same source code file more than once. Since a class can be defined only once, using such a preprocessing directive prevents multiple-definition errors.

14. In professional C++ development, rather than building your own classes to represent times and dates, you’ll typically use the header <chrono> (https://en.cppreference.com/w/cpp/chrono) from the C++ Standard Library.

20 C++20 Modules Change How You Separate Interface from Implementation

Images MOD As you’ll see in Chapter 15, C++20 modules15 eliminate the need for preprocessor #pragma once. You’ll also see that modules enable you to separate interface from implementation in a single source-code file or by using multiple source-code files.

15. At the time of this writing, the C++20 modules features were not fully implemented by the three compilers we use, so we cover them in a separate chapter.

9.7.1 Interface of a Class

Interfaces define and standardize how things such as people and systems interact with one another. For example, a radio’s controls serve as an interface between the radio’s users and its internal components. The controls allow users to perform a limited set of operations (such as changing the station, adjusting the volume, and choosing between AM and FM stations). Various radios may implement these operations differently—some provide push buttons, some provide dials and some support voice commands. The interface specifies what operations a radio permits users to perform but does not specify how the operations are implemented inside the radio.

Similarly, the interface of a class describes what services a class’s clients can use and how to request those services, but not how the class implements them. A class’s public interface consists of the class’s public member functions (also known as the class’s public services). As you’ll soon see, you can specify a class’s interface by writing a class definition that lists only the class’s member-function prototypes in the class’s public section.

9.7.2 Separating the Interface from the Implementation

To separate the class’s interface from its implementation, we break up class Time into two files—Time.h (Fig. 9.7) in which class Time is defined and Time.cpp (Fig. 9.8) in which Time’s member functions are defined. This split:

1. helps make the class reusable,

2. ensures that the clients of the class know what member functions the class provides, how to call them and what return types to expect, and

3. enables the clients to ignore how the class’s member functions are implemented.

 1   // Fig. 9.7: Time.h
 2   // Time class definition.
 3   // Member functions are defined in Time.cpp
 4   #pragma once // prevent multiple inclusions of header
 5   #include <string>
 6
 7   // Time class definition
 8   class Time {
 9   public:
10      void setTime(int hour, int minute, int second);
11      std::string to24HourString() const; // 24-hour string format
12      std::string to12HourString() const; // 12-hour string format
13   private:
14      int m_hour{0}; // 0 - 23 (24-hour clock format)
15      int m_minute{0}; // 0 - 59
16      int m_second{0}; // 0 - 59
17   };

Fig. 9.7 Time class definition.

 1   // Fig. 9.8: Time.cpp
 2   // Time class member-function definitions.
 3   #include <stdexcept> // for invalid_argument exception class
 4   #include <string>
 5   #include <fmt/format.h> // In C++20, this will be #include <format>
 6   #include "Time.h" // include definition of class Time from Time.h
 7
 8   using namespace std;
 9
10   // set new Time value using 24-hour time
11   void Time::setTime(int hour, int minute, int second) {
12      // validate hour, minute and second
13      if ((hour < 0 || hour >= 24) || (minute < 0 || minute >= 60) ||
14         (second < 0 || second >= 60)) {
15         throw invalid_argument{"hour, minute or second was out of range"};
16      }
17
18      m_hour = hour;
19      m_minute = minute;
20      m_second = second;
21   }
22
23   // return Time as a string in 24-hour format (HH:MM:SS)
24   string Time::to24HourString() const {
25      return fmt::format("{:02d}:{:02d}:{:02d}", m_hour, m_minute, m_second);
26   }
27
28   // return Time as string in 12-hour format (HH:MM:SS AM or PM)
29   string Time::to12HourString() const {
30      return fmt::format("{}:{:02d}:{:02d} {}",
31                ((m_hour % 12 == 0) ? 12 : m_hour % 12), m_minute, m_second,
32                (m_hour < 12 ? "AM" : "PM"));
33   }

Fig. 9.8 Time class member-function definitions.

Images PERF In addition, this split can reduce compilation time because the implementation file can be compiled, then does not need to be recompiled unless the implementation changes.

By convention, member-function definitions are placed in a .cpp file with the same base name (e.g., Time) as the class’s header. Some compilers support other filename extensions as well. Figure 9.9 defines function main, which is the client of our Time class.

 1   // fig09_09.cpp
 2   // Program to test class Time.
 3   // NOTE: This file must be compiled with Time.cpp.
 4   #include <iostream>
 5   #include <stdexcept> // invalid_argument exception class
 6   #include <string_view>
 7   #include <fmt/format.h> // In C++20, this will be #include <format>
 8   #include "Time.h" // definition of class Time from Time.h
 9   using namespace std;
10
11   // displays a Time in 24-hour and 12-hour formats
12   void displayTime(string_view message, const Time& time) {
13      cout << fmt::format("{}
24-hour time: {}
12-hour time: {}

",
14                 message, time.to24HourString(), time.to12HourString());
15   }
16
17   int main() {
18      Time t{}; // instantiate object t of class Time
19
20      displayTime("Initial time:", t); // display t's initial value
21      t.setTime(13, 27, 6); // change time
22      displayTime("After setTime:", t); // display t's new value
23
24      // attempt to set the time with invalid values
25      try {
26         t.setTime(99, 99, 99); // all values out of range
27      }
28      catch (const invalid_argument& e) {
29         cout << fmt::format("Exception: {}

", e.what());
30      }
31
32      // display t's value after attempting to set an invalid time
33      displayTime("After attempting to set an invalid time:", t);
34   }
Initial time:
24-hour time: 00:00:00
12-hour time: 12:00:00 AM

After setTime:
24-hour time: 13:27:06
12-hour time: 1:27:06 PM

Exception: hour, minute and/or second was out of range

After attempting to set an invalid time:
24-hour time: 13:27:06
12-hour time: 1:27:06 PM

Fig. 9.9 Program to test class Time.

9.7.3 Class Definition

The header Time.h (Fig. 9.7) contains Time’s class definition (lines 8–17). Rather than function definitions, the class contains function prototypes (lines 10–12) that describe the class’s public interface without revealing the member-function implementations. The function prototype in line 10 indicates that setTime requires three int parameters and returns void. The prototypes for to24HourString and to12HourString (lines 11–12) specify that they take no arguments and return a string. Classes with one or more constructors would also declare them in the header, as we’ll do in subsequent examples.

11 The header still specifies the class’s private data members (lines 14–16). Each uses a C++11 in-class initializer to set the data member to 0. The compiler must know the class’s data members to determine how much memory to reserve for each object of the class. Including the header Time.h in the client code provides the compiler with the information it needs to ensure that the client code calls class Time’s member functions correctly.

#pragma once

Images ERR Images MOD In larger programs, headers also will contain other definitions and declarations. Attempts to include a header multiple times (inadvertently) often occur in programs with many headers that may themselves include other headers. This could lead to compilation errors if the same definition appears more than once in a preprocessed file. The #pragma once directive (line 4) prevents time.h’s contents from being included into the same source-code file more than once. In Chapter 15, we’ll discuss how C++20 modules help prevent such problems.

9.7.4 Member Functions

Time.cpp (Fig. 9.8) defines class Time’s member functions, which were declared in lines 10–12 of Fig. 9.7. For member functions to24HourString and to12HourString, the const keyword must appear in both the function prototypes (Fig. 9.7, lines 11–12) and the function definitions (Fig. 9.8, lines 24 and 29).

9.7.5 Including the Class Header in the Source-Code File

To indicate that the member functions in Time.cpp are part of class Time, we must first include the Time.h header (Fig. 9.8, line 6). This allows us to use the class name Time in the Time.cpp file (lines 11, 24 and 29). When compiling Time.cpp, the compiler uses the information in Time.h to ensure that

• the first line of each member function matches its prototype in Time.h, and

• each member function knows about the class’s data members and other member functions.

9.7.6 Scope Resolution Operator (::)

Each member function’s name (lines 11, 24 and 29) is preceded by the class name and the scope resolution operator (::). This “ties” them to the (now separate) Time class definition (Fig. 9.7), which declares the class’s members. The Time:: tells the compiler that each member function is in that class’s scope, and its name is known to other class members.

Without “Time::” preceding each function name, the compiler would treat these as global functions with no relationship to class time. Such functions, also called “free” functions, cannot access Time’s private data and cannot call the class’s member functions without specifying an object. So, the compiler would not be able to compile these functions because it would not know that class Time declares variables m_hour, m_minute and m_second. In Fig. 9.8, lines 18–20, 25 and 31–32 would cause compilation errors because m_hour, m_minute and m_second are not declared as local variables in each function nor are they declared as global variables.

9.7.7 Member Function setTime and Throwing Exceptions

Function setTime (lines 11–21) is a public function that declares three int parameters and uses them to set the time. Lines 13–14 test each argument to determine whether the value is in range. If so, lines 18–20 assign the values to the m_hour, m_minute and m_second data members, respectively. The hour argument must be greater than or equal to 0 and less than 24 because the 24-hour time format represents hours as integers from 0 to 23. Similarly, the minute and second arguments must be greater than or equal to 0 and less than 60.

If any of the values is outside its range, setTime throws an exception (lines 15) of type invalid_argument (header <stdexcept>), which notifies the client code that an invalid argument was received. As you saw in Section 6.15, you can use trycatch to catch exceptions and attempt to recover from them, which we’ll do in Fig. 9.9. The throw statement creates a new invalid_argument object. That object’s constructor receives a custom error-message string. After the exception object is created, the throw statement terminates function setTime. Then, the exception is returned to the code that called setTime.

Invalid values cannot be stored in a Time object, because:

• when a Time object is created, its default constructor is called and each data member is initialized to 0, as specified in lines 14–16 of Fig. 9.7—setting m_hour, m_minute and m_second to 0 is the equivalent of 12 AM (midnight)—and

• all subsequent attempts by a client to modify the data members are scrutinized by function setTime.

9.7.8 Member Function to24HourString

Member function to24HourString (lines 24–26 of Fig. 9.8) takes no arguments and returns a formatted 24-hour string with three colon-separated digit pairs. So, if the time is 1:30:07 PM, the function returns "13:30:07". Each {:02d} placeholder in line 25 formats an integer (d) in a field width of two. The 0 before the field width indicates that values with fewer than two digits should be formatted with leading zeros.

9.7.9 Member Function to12HourString

Function to12HourString (lines 29–33) takes no arguments and returns a formatted 12-hour time string containing the m_hour, m_minute and m_second values separated by colons and followed by an AM or PM indicator (e.g., 10:54:27 AM and 1:27:06 PM). The function uses the placeholder {:02d} to format m_minute and m_second as two-digit values with leading zeros, if necessary. Line 31 uses the conditional operator (?:) to determine how m_hour should be formatted. If m_hour is 0 or 12 (AM or PM, respectively), it appears as 12; otherwise, we use the remainder operator (%) to get a value from 1 to 11. The conditional operator in line 32 determines whether to include AM or PM.

9.7.10 Implicitly Inlining Member Functions

Images PERF If a member function is fully defined in a class’s body (as we did in our Account class examples), the member function is implicitly declared inline (Section 5.12). This can improve performance. Remember that the compiler reserves the right not to inline any function. Similarly, optimizing compilers also reserve the right to inline functions even if they are not declared with the inline keyword.

Only the simplest, most stable member functions (i.e., whose implementations are unlikely to change) should be defined in the class header. Every change to the header requires you to recompile every source-code file dependent on that header—a time-consuming task in large systems.

9.7.11 Member Functions vs. Global Functions

Images SE Member functions to24HourString and to12HourString take no arguments. They implicitly know about and are able to access the data members for the Time object on which they’re invoked. This is a nice benefit of object-oriented programming. In general, member-function calls receive either no arguments or fewer arguments than function calls in non-object-oriented programs. This reduces the likelihood of passing wrong arguments, the wrong number of arguments or arguments in the wrong order.

9.7.12 Using Class Time

Once class Time is defined, it can be used as a type in declarations, such as:

Time sunset{}; // object of type Time
array<Time, 5> arrayOfTimes{}; // std::array of 5 Time objects
Time& dinnerTimeRef{sunset}; // reference to a Time object
Time* timePtr{&sunset}; // pointer to a Time object

Figure 9.9 creates and manipulates a Time object. Separating Time’s interface from the implementation of its member functions does not affect the way that this client code uses the class. Line 8 includes Time.h so the compiler knows how much space to reserve for the Time object t (line 18) and can ensure that Time objects are created and manipulated correctly in the client code.

Throughout the program, we display string representations of the Time object using function displayTime (lines 12–15), which calls Time member functions to24HourString and to12HourString. Line 18 creates the Time object t. Recall that class Time does not define a constructor, so this statement calls the compiler-generated default constructor. Thus, t’s m_hour, m_minute and m_second are set to 0 via their initializers in class Time’s definition. Then, line 20 displays the time in 24-hour and 12-hour formats, respectively, to confirm that the members were correctly initialized. Line 21 sets a new valid time by calling member function setTime, and line 22 again shows the time in both formats.

Calling setTime with Invalid Values

To show that setTime validates its arguments, line 26 calls setTime with invalid arguments of 99 for the hour, minute and second parameters. We placed this statement in a try block (lines 25–27) in case setTime throws an invalid_argument exception, which it will do in this example. When the exception occurs, it’s caught at lines 28–30, and line 29 displays the exception’s error message by calling its what member function. Line 33 shows the time to confirm that setTime did not change the time when invalid arguments were supplied.

9.7.13 Object Size

People new to object-oriented programming often suppose that objects must be quite large because they contain data members and member functions. Logically, this is true. You may think of objects as containing data and functions physically (and our discussion has certainly encouraged this view). However, this is not the case.

Images PERF Objects contain only data, so they’re much smaller than if they also contained member functions. The member functions’ code is maintained separately from all objects of the class. Each object needs its own data because the data usually varies among the objects. The function code is the same for all objects of the class and can be shared among them.

9.8 Compilation and Linking Process

Often a class’s interface and implementation will be created by one programmer and used by a separate programmer who implements the client code. A class-implementation programmer responsible for creating a reusable Time class creates the header Time.h and the source-code file Time.cpp that #includes the header, then provides these files to the client-code programmer. A reusable class’s source code often is available to client-code programmers as a library they can download from a website, like GitHub.com.

The client-code programmer needs to know only Time’s interface to use the class and must be able to compile Time.cpp and link its object code. Since the class’s interface is part of the class definition in the Time.h header, the client-code programmer must #include this file in the client’s source-code file. The compiler uses the class definition in Time.h to ensure that the client code creates and manipulates Time objects correctly.

To create the executable Time application, the last step is to link

1. the object code for the main function (that is, the client code),

2. the object code for class Time’s member-function implementations, and

3. the C++ Standard Library object code for the C++ classes (such as std::string) used by the class-implementation programmer and the client-code programmer.

Images SE The linker’s output for the program of Section 9.7 is the executable application that users can run to create and manipulate a Time object. Compilers and IDEs typically invoke the linker for you after compiling your code.

Compiling Programs Containing Two or More Source-Code Files

In Section 1.9, we showed how to compile and run C++ applications that contained one source-code (.cpp) file. To compile and link multiple source-code files:16

• In Microsoft Visual Studio, add to your project (as shown in Section 1.9.1) all the custom headers and source-code files that make up the program, then build and run the project. You can place the headers in the project’s Header Files folder and the source-code files in the project’s Source Files folder, but these are mainly for organizing files in large projects. The programs will compile if you place all the files in the Source Files folder.

• For GNU C++, open a shell and change to the directory containing all the files for a given program. Then in your compilation command, either list each .cpp file by name or use *.cpp to compile all the .cpp files in the current folder. The preprocessor automatically locates the program-specific headers in that folder.

• For Apple Xcode, add to your project (as shown in Section 1.9.2) all the headers and source-code files that make up a program, then build and run the project.

Images MOD 16. This process changes with C++20 modules, as we’ll discuss in Chapter 15.

9.9 Class Scope and Accessing Class Members

A class’s data members and member functions belong to that class’s scope. Non-member functions are defined at global namespace scope, by default. (We discuss namespaces in more detail in Section 19.4.) Within a class’s scope, class members are immediately accessible by all of that class’s member functions and can be referenced by name. Outside a class’s scope, public class members are referenced through

• an object name,

• a reference to an object, or

• a pointer to an object.

We refer to these as handles on an object. The handle’s type helps the compiler determine the interface (that is, the member functions) accessible to the client via that handle. We’ll see in Section 9.19 that an implicit handle (called the this pointer) is inserted by the compiler each time you refer to a data member or member function from within an object.

Dot (.) and Arrow (->) Member-Selection Operators

As you know, you can use an object’s name or a reference to an object followed by the dot member-selection operator (.) to access the object’s members. To reference an object’s members via a pointer to an object, follow the pointer name by the arrow member-selection operator (->) and the member name, as in pointerName->memberName.

Accessing public Class Members Through Objects, References and Pointers

Consider an Account class that has a public deposit member function. Given the following declarations:

Account account{}; // an Account object
Account& ref{account}; // ref refers to an Account object
Account* ptr{&account}; // ptr points to an Account object

You can invoke member function deposit using the dot (.) and arrow (->) member selection operators as follows:

account.deposit(123.45); // call deposit via account object
ref.deposit(123.45); // call deposit via reference to account
ptr->deposit(123.45); // call deposit via pointer to account

Again, you should use references over pointers whenever possible. We’ll continue to show pointers when they are required and to prepare you to work with them in the legacy code you’ll encounter in industry.

9.10 Access Functions and Utility Functions

Access Functions

Access functions can read or display data, not modify it. Another use of access functions is to test whether a condition is true or false. Such functions are often called predicate functions. An example would be a std::array’s or a std::vector’s empty function. A program might test empty before attempting to read an item from the container object.17

17. Many programmers prefer to begin the names of predicate functions with the word “is.” For example, useful predicate functions for our Time class might be isAM and isPM.

Utility Functions

Images SE A utility function (also called a helper function) is a private member function that supports the operation of a class’s other member functions. Utility functions are declared private because they’re not intended for use by the class’s clients. Typically, a utility function contains code that would otherwise be duplicated in several other member functions.

9.11 Time Class Case Study: Constructors with Default Arguments

The program of Figs. 9.109.12 enhances class Time to demonstrate a constructor with default arguments.

 1   // Fig. 9.10: Time.h
 2   // Time class containing a constructor with default arguments.
 3   // Member functions defined in Time.cpp.
 4   #pragma once // prevent multiple inclusions of header
 5   #include <string>
 6
 7   // Time class definition
 8   class Time {
 9   public:
10      // default constructor because it can be called with no arguments
11      explicit Time(int hour = 0, int minute = 0, int second = 0);
12
13      // set functions
14      void setTime(int hour, int minute, int second);
15      void setHour(int hour); // set hour (after validation)
16      void setMinute(int minute); // set minute (after validation)
17      void setSecond(int second); // set second (after validation)
18
19      // get functions
20      int getHour() const; // return hour
21      int getMinute() const; // return minute
22      int getSecond() const; // return second
23
24      std::string to24HourString() const; // 24-hour time format string
25      std::string to12HourString() const; // 12-hour time format string
26   private:
27      int m_hour{0}; // 0 - 23 (24-hour clock format)
28      int m_minute{0}; // 0 - 59
29      int m_second{0}; // 0 - 59
30   };

Fig. 9.10 Time class containing a constructor with default arguments.

 1   // Fig. 9.11: Time.cpp
 2   // Member-function definitions for class Time.
 3   #include <stdexcept>
 4   #include <string>
 5   #include <fmt/format.h> // In C++20, this will be #include <format>
 6   #include "Time.h" // include definition of class Time from Time.h
 7   using namespace std;
 8
 9   // Time constructor initializes each data member
10   Time::Time(int hour, int minute, int second) {
11      setHour(hour); // validate and set private field m_hour
12      setMinute(minute); // validate and set private field m_minute
13      setSecond(second); // validate and set private field m_second
14   }
15
16   // set new Time value using 24-hour time
17   void Time::setTime(int hour, int minute, int second) {
18      // validate hour, minute and second
19      if ((hour < 0 || hour >= 24) || (minute < 0 || minute >= 60) ||
20         (second < 0 || second >= 60)) {
21         throw invalid_argument{"hour, minute or second was out of range"};
22      }
23
24      m_hour = hour;
25      m_minute = minute;
26      m_second = second;
27   }
28
29   // set hour value
30   void Time::setHour(int hour) {
31      if (hour < 0 || hour >= 24) {
32         throw invalid_argument{"hour must be 0-23"};
33      }
34
35      m_hour = hour;
36   }
37
38   // set minute value
39   void Time::setMinute(int minute) {
40      if (minute < 0 || minute >= 60) {
41         throw invalid_argument{"minute must be 0-59"};
42      }
43
44      m_minute = minute;
45   }
46
47   // set second value
48   void Time::setSecond(int second) {
49      if (second < 0 && second >= 60) {
50         throw invalid_argument{"second must be 0-59"};
51      }
52
53      m_second = second;
54   }
55
56   // return hour value
57   int Time::getHour() const {return m_hour;}
58
59   // return minute value
60   int Time::getMinute() const {return m_minute;}
61
62   // return second value
63   int Time::getSecond() const {return m_second;}
64
65   // return Time as a string in 24-hour format (HH:MM:SS)
66   string Time::to24HourString() const {
67      return fmt::format("{:02d}:{:02d}:{:02d}",
68                getHour(), getMinute(), getSecond());
69   }
70
71   // return Time as string in 12-hour format (HH:MM:SS AM or PM)
72   string Time::to12HourString() const {
73      return fmt::format("{}:{:02d}:{:02d} {}",
74         ((getHour() % 12 == 0) ? 12 : getHour() % 12),
75         getMinute(), getSecond(), (getHour() < 12 ? "AM" : "PM"));
76   }

Fig. 9.11 Member-function definitions for class Time.

 1   // fig09_12.cpp
 2   // Constructor with default arguments.
 3   #include <iostream>
 4   #include <stdexcept>
 5   #include <string>
 6   #include <fmt/format.h> // In C++20, this will be #include <format>
 7   #include "Time.h" // include definition of class Time from Time.h
 8   using namespace std;
 9
10   // displays a Time in 24-hour and 12-hour formats
11   void displayTime(string_view message, const Time& time) {
12      cout << fmt::format("{}
24-hour time: {}
12-hour time: {}

",
13                 message, time.to24HourString(), time.to12HourString());
14   }
15
16   int main() {
17      const Time t1{}; // all arguments defaulted
18      const Time t2{2}; // hour specified; minute and second defaulted
19      const Time t3{21, 34}; // hour and minute specified; second defaulted
20      const Time t4{12, 25, 42}; // hour, minute and second specified
21
22      cout << "Constructed with:

";
23      displayTime("t1: all arguments defaulted", t1);
24      displayTime("t2: hour specified; minute and second defaulted", t2);
25      displayTime("t3: hour and minute specified; second defaulted", t3);
26      displayTime("t4: hour, minute and second specified", t4);
27
28      // attempt to initialize t5 with invalid values
29      try {
30         const Time t5{27, 74, 99}; // all bad values specified
31      }
32      catch (const invalid_argument& e) {
33         cerr << fmt::format("t5 not created: {}
", e.what());
34      }
35   }
Constructed with:

t1: all arguments defaulted
24-hour time: 00:00:00
12-hour time: 12:00:00 AM

t2: hour specified; minute and second defaulted
24-hour time: 02:00:00
12-hour time: 2:00:00 AM

t3: hour and minute specified; second defaulted
24-hour time: 21:34:00
12-hour time: 9:34:00 PM

t4: hour, minute and second specified
24-hour time: 12:25:42
12-hour time: 12:25:42 PM

t5 not created: hour must be 0-23

Fig. 9.12 Constructor with default arguments.

9.11.1 Class Time

Images SE Like other functions, constructors can specify default arguments. Line 11 of Fig. 9.10 declares a Time constructor with default arguments, specifying the default value 0 for each parameter. A constructor with default arguments for all its parameters 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. Any change to a function’s default argument values requires the client code to be recompiled (to ensure that the program still functions correctly). Class Time’s constructor is declared explicit because it can be called with one argument. We discuss explicit constructors in detail in Section 11.13. This Time class also provides set and get functions for each data member (lines 15–17 and 20–22).

Class Time’s Constructor

In Fig. 9.11, lines 10–14 define the Time constructor. The Time constructor calls the setHour, setMinute and setSecond functions to validate and assign values to the data members.

Images ERR Lines 11–13 of the constructor call setHour to ensure that hour is in the range 0–23, then calls setMinute and setSecond to ensure that minute and second are each in the range 0–59. Functions setHour (lines 30–36), setMinute (lines 39–45) and setSecond (lines 48–54) each throw an exception if an out-of-range argument is received. If setHour, setMinute or setSecond throws an exception during construction, the Time object will not complete construction and will not exist for use in the program.

Images CG Images ERR The C++ Core Guidelines provide many constructor recommendations. We’ll see more in the next two chapters. If a class requires data members to have specific values (as in class Time), the class should define a constructor that validates the data and, if invalid, throws an exception.18,19,20

18. C++ Core Guidelines. Accessed July 12, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-ctor.

19. C++ Core Guidelines. Accessed July 12, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-complete.

20. C++ Core Guidelines. Accessed July 12, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-throw.

Testing the Updated Class Time

Function main in Fig. 9.12 initializes five Time objects:

• one with all three arguments defaulted in the implicit constructor call (line 17),

• one with one argument specified (line 18),

• one with two arguments specified (line 19),

• one with three arguments specified (line 20) and

• one with three invalid arguments specified (line 30).

Images ERR The program displays each object in 24-hour and 12-hour time formats. For Time object t5 (line 30), the program displays an error message because the constructor arguments are out of range. The variable t5 never represents a fully constructed object in this program, because the exception is thrown during construction.

Images SE Software Engineering Notes Regarding Class Time’s set and get Functions and Constructor

Time’s set and get functions are called throughout the class’s body. In particular, the constructor (Fig. 9.11, lines 10–14) calls functions setHour, setMinute and setSecond, and functions to24HourString and to12HourString call functions getHour, getMinute and getSecond in lines 68 and lines 74–75. In each case, we could have accessed the class’s private data directly.

Images SE Our current internal time representation uses three ints, which require 12 bytes of memory on systems with four-byte ints. Consider changing this to a single int representing the total number of seconds since midnight, which requires only four bytes of memory. If we made this change, only the bodies of functions that directly access the private data would need to change. In this class, we’d modify setTime and the set and get function’s bodies for m_hour, m_minute and m_second. There would be no need to modify the constructor or functions to24HourString or to12HourString because they do not access the data directly.

Images ERR Duplicating statements in multiple functions or constructors makes changing the class’s internal data representation more difficult. Implementing the Time constructor and functions to24HourString and to12HourString as shown in this example reduces the likelihood of errors when altering the class’s implementation.

Images SE As a general rule: Avoid repeating code. This principle is generally referred to as DRY—“don’t repeat yourself.”21 Rather than duplicating code, place it in a member function that can be called by the class’s constructor or other member functions. This simplifies code maintenance and reduces the likelihood of an error if the code implementation is modified.

21. “Don't Repeat Yourself,” Wikipedia (Wikimedia Foundation, Accessed June 20, 2020), https://en.wikipedia.org/wiki/Don't_repeat_yourself.

Images SE Images ERR A constructor can call the class’s other member functions. You must be careful when doing this. The constructor initializes the object, so data members used in the called function may not yet be initialized. Logic errors may occur if you use data members before they have been properly initialized.

Images SE 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 private. You must provide appropriate validity checking.

11 9.11.2 Overloaded Constructors and C++11 Delegating Constructors

Section 5.16 showed how to overload functions. A class’s constructors and member functions also can be overloaded. Overloaded constructors allow objects to be initialized with different types and/or numbers of arguments. To overload a constructor, provide a prototype and definition for each overloaded version. This also applies to overloaded member functions.

In Figs. 9.109.12, class Time’s constructor had a default argument for each parameter. We could have defined that constructor instead as four overloaded constructors with the following prototypes:

Time(); // default m_hour, m_minute and m_second to 0
explicit Time(int hour); // default m_minute & m_second to 0
Time(int hour, int minute); // default m_second to 0
Time(int hour, int minute, int second); // no default values

Images CG Just as a constructor can call a class’s other member functions to perform tasks, constructors can call other constructors in the same class. The calling constructor is known as a delegating constructor—it delegates its work to another constructor. The C++ Core Guidelines recommend defining common code for overloaded constructors in one constructor, then using delegating constructors to call it.22 Before C++11, this would have been accomplished via a private utility function called by all the constructors.

22. C++ Core Guidelines. Accessed July 12, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-delegating.

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

Time::Time(int hour) : Time{hour, 0, 0} {}

Time::Time(int hour, int minute) : Time{hour, minute, 0} {}

9.12 Destructors

A destructor is a special member function that may not specify parameters or a return type. A class’s destructor name is the tilde character (~) followed by the class name, such as ~Time. This naming convention has intuitive appeal because, as we’ll see in a later chapter, the tilde is the bitwise complement operator. In a sense, the destructor is the complement of the constructor.

A class’s destructor is called implicitly when an object is destroyed, typically when program execution leaves the scope in which that object was created. The destructor itself does not actually remove the object from memory. It performs termination housekeeping (such as closing a file) before the object’s memory is reclaimed for later use.

Even though destructors have not been defined for the classes presented so far, every class has exactly one destructor. If you do not explicitly define a destructor, the compiler defines a default destructor that invokes any class-type data members’ destructors.23 In Chapter 12, we’ll explain why exceptions should not be thrown from destructors.

23. We’ll see that such a default destructor also destroys class objects that are created through inheritance (Chapter 10).

9.13 When Constructors and Destructors Are Called

Images SE Constructors and destructors are called implicitly when objects are created and when they’re about to go out of scope, respectively. The order in which these are called depends on the objects’ scopes. Generally, destructor calls are made in the reverse order of the corresponding constructor calls, but as we’ll see in Figs. 9.139.15, global and static objects can alter the order in which destructors are called.

 1   // Fig. 9.13: CreateAndDestroy.h
 2   // CreateAndDestroy class definition.
 3   // Member functions defined in CreateAndDestroy.cpp.
 4   #pragma once // prevent multiple inclusions of header
 5   #include <string>
 6   #include <string_view>
 7
 8   class CreateAndDestroy {
 9   public:
10      CreateAndDestroy(int ID, std::string_view message); // constructor
11      ~CreateAndDestroy(); // destructor
12   private:
13      int m_ID; // ID number for object
14      std::string m_message; // message describing object
15   };

Fig. 9.13 CreateAndDestroy class definition.

 1   // Fig. 9.14: CreateAndDestroy.cpp
 2   // CreateAndDestroy class member-function definitions.
 3   #include <iostream>
 4   #include <fmt/format.h> // In C++20, this will be #include <format>
 5   #include "CreateAndDestroy.h"// include CreateAndDestroy class definition
 6   using namespace std;
 7
 8   // constructor sets object's ID number and descriptive message
 9   CreateAndDestroy::CreateAndDestroy(int ID, string_view message)
10      : m_ID{ID}, m_message{message} {
11      cout << fmt::format("Object {}  constructor runs  {}
",
12                 m_ID, m_message);
13   }
14
15   // destructor
16   CreateAndDestroy::~CreateAndDestroy() {
17      // output newline for certain objects; helps readability
18      cout << fmt::format("{}Object {}  destructor runs  {}
",
19                 (m_ID == 1 || m_ID == 6 ? "
" : ""), m_ID, m_message);
20   }

Fig. 9.14 CreateAndDestroy class member-function definitions.

 1   // fig09_15.cpp
 2   // Order in which constructors and
 3   // destructors are called.
 4   #include <iostream>
 5   #include "CreateAndDestroy.h" // include CreateAndDestroy class definition
 6   using namespace std;
 7
 8   void create(); // prototype
 9
10   const CreateAndDestroy first{1, "(global before main)"}; // global object
11
12   int main() {
13      cout << "
MAIN FUNCTION: EXECUTION BEGINS
";
14      const CreateAndDestroy second{2, "(local in main)"};
15      static const CreateAndDestroy third{3, "(local static in main)"};
16
17      create(); // call function to create objects
18
19      cout << "
MAIN FUNCTION: EXECUTION RESUMES
";
20      const CreateAndDestroy fourth{4, "(local in main)"};
21      cout << "
MAIN FUNCTION: EXECUTION ENDS
";
22   }
23
24   // function to create objects
25   void create() {
26      cout << "
CREATE FUNCTION: EXECUTION BEGINS
";
27      const CreateAndDestroy fifth{5, "(local in create)"};
28      static const CreateAndDestroy sixth{6, "(local static in create)"};
29      const CreateAndDestroy seventh{7, "(local in create)"};
30      cout << "
CREATE FUNCTION: EXECUTION ENDS
";
31   }
Object 1   constructor runs   (global before main)

MAIN FUNCTION: EXECUTION BEGINS
Object 2   constructor runs   (local in main)
Object 3   constructor runs   (local static in main)

CREATE FUNCTION: EXECUTION BEGINS
Object 5   constructor runs   (local in create)
Object 6   constructor runs   (local static in create)
Object 7   constructor runs   (local in create)

CREATE FUNCTION: EXECUTION ENDS
Object 7   destructor runs    (local in create)
Object 5   destructor runs    (local in create)

MAIN FUNCTION: EXECUTION RESUMES
Object 4   constructor runs   (local in main)

MAIN FUNCTION: EXECUTION ENDS
Object 4   destructor runs    (local in main)
Object 2   destructor runs    (local in main)

Object 6   destructor runs    (local static in create)
Object 3   destructor runs    (local static in main)

Object 1   destructor runs    (global before main)

Fig. 9.15 Order in which constructors and destructors are called.

Constructors and Destructors for Objects in Global Scope

Images SE Images ERR Images ERR Constructors are called for objects defined in global scope (also called global namespace scope) before any other function (including main) in that file begins execution. The execution order of global object constructors among multiple files is not guaranteed. When main terminates, the corresponding destructors are called in the reverse order of their construction. The exit function often is used to terminate a program when a fatal unrecoverable error occurs. Function exit forces a program to terminate immediately and does not execute the destructors of local objects. Function abort performs similarly to function exit but forces the program to terminate immediately, without allowing programmer-defined cleanup code of any kind to be called. Function abort is usually used to indicate abnormal termination of the program. (See Appendix G for more information on functions exit and abort.)

Constructors and Destructors for Non-static Local Objects

A non-static local object’s constructor is called when execution reaches the object’s definition. Its destructor is called when execution leaves the object’s scope—that is, when the block in which that object is defined finishes executing normally or due to an exception. Destructors are not called for local objects if the program terminates with a call to function exit or function abort.

Constructors and Destructors for static Local Objects

The constructor for a static local object is called only once when execution first reaches the point where the object is defined. The corresponding destructor is called when main terminates or the program calls function exit. Global and static objects are destroyed in the reverse order of their creation. Destructors are not called for static objects if the program terminates with a call to function abort.

Demonstrating When Constructors and Destructors Are Called

The program of Figs. 9.139.15 demonstrates the order in which constructors and destructors are called for global, local and local static objects of class CreateAndDestroy (Fig. 9.13 and Fig. 9.14). This mechanical example is purely for pedagogic purposes.

Figure 9.13 declares class CreateAndDestroy. Lines 13–14 declare the class’s data members—an integer (m_ID) and a string (m_message) that identify each object in the program’s output.

The constructor and destructor implementations (Fig. 9.14) both display lines of output to indicate when they’re called. In the destructor, the conditional expression (line 19) determines whether the object being destroyed has the m_ID value 1 or 6 and, if so, outputs a newline character to make the program’s output easier to follow.

Figure 9.15 defines object first (line 10) in global scope. Its constructor is called before any statements in main execute and its destructor is called at program termination after the destructors for all objects with automatic storage duration have run.

Function main (lines 12–22) defines three objects. Objects second (line 14) and fourth (line 20) are local objects, and object third (line 15) is a static local object. The constructor for each of these objects is called when execution reaches the point where that object is defined. When execution reaches the end of main, the destructors for objects fourth then second are called in the reverse of their constructors’ order. Object third is static, so it exists until program termination. The destructor for object third is called before the destructor for global object first, but after non-static local objects are destroyed.

Function create (lines 25–31) defines three objects—fifth (line 27) and seventh (line 29) are local automatic objects, and sixth (line 28) is a static local object. When create terminates, the destructors for objects seventh then fifth are called in the reverse of their constructors’ order. Because sixth is static, it exists until program termination. The destructor for sixth is called before the destructors for third and first, but after all other objects are destroyed.

9.14 Time Class Case Study: A Subtle Trap — Returning a Reference or a Pointer to a private Data Member

A reference to an object is an alias for the object’s name, so it may be used on the left side of an assignment statement. In this context, the reference makes a perfectly acceptable lvalue to which you can assign a value.

A member function can return a reference to a private data member of that class. If the reference return type is declared const, the reference is a nonmodifiable lvalue and cannot be used to modify the data. However, if the reference return type is not declared const, subtle errors can occur.

The program of Figs. 9.169.18 uses a simplified Time class (Fig. 9.16 and Fig. 9.17) to demonstrate returning a reference to a private data member with member function badSetHour (declared in Fig. 9.16 in line 12 and defined in Fig. 9.17 in lines 24–31). Such a reference return makes the result of a call to member function badSetHour an alias for private data member hour! The function call can be used in any way that the private data member can be used, including as an lvalue in an assignment statement, thus enabling clients of the class to overwrite the class’s private data at will! A similar problem would occur if the function returned a pointer to the private data.

 1   // Fig. 9.16: Time.h
 2   // Time class definition.
 3   // Member functions defined in Time.cpp
 4
 5   // prevent multiple inclusions of header
 6   #pragma once
 7
 8   class Time {
 9   public:
10      void setTime(int hour, int minute, int second);
11      int getHour() const;
12      int& badSetHour(int h); // dangerous reference return
13   private:
14      int m_hour{0};
15      int m_minute{0};
16      int m_second{0};
17   };

Fig. 9.16 Time class declaration.

 1   // Fig. 9.17: Time.cpp
 2   // Time class member-function definitions.
 3   #include <stdexcept>
 4   #include "Time.h" // include definition of class Time
 5   using namespace std;
 6
 7   // set values of hour, minute and second
 8   void Time::setTime(int hour, int minute, int second) {
 9      // validate hour, minute and second
10      if ((hour < 0 || hour >= 24) || (minute < 0 || minute >= 60) ||
11         (second < 0 || second >= 60)) {
12         throw invalid_argument{"hour, minute or second was out of range"};
13      }
14
15      m_hour = hour;
16      m_minute = minute;
17      m_second = second;
18   }
19
20   // return hour value
21   int Time::getHour() const {return m_hour;}
22
23   // poor practice: returning a reference to a private data member.
24   int& Time::badSetHour(int hour) {
25      if (hour < 0 || hour >= 24) {
26         throw invalid_argument{"hour must be 0-23"};
27      }
28
29      m_hour = hour;
30      return m_hour; // dangerous reference return
31   }

Fig. 9.17 Time class member-function definitions.

 1   // fig09_18.cpp
 2   // public member function that
 3   // returns a reference to private data.
 4   #include <iostream>
 5   #include <fmt/format.h>
 6   #include "Time.h" // include definition of class Time
 7   using namespace std;
 8
 9   int main() {
10      Time t{}; // create Time object
11
12      // initialize hourRef with the reference returned by badSetHour
13      int& hourRef{t.badSetHour(20)}; // 20 is a valid hour
14
15      cout << fmt::format("Valid hour before modification: {}
", hourRef);
16      hourRef = 30; // use hourRef to set invalid value in Time object t
17      cout << fmt::format("Invalid hour after modification: {}

",
18                 t.getHour());
19
20      // Dangerous: Function call that returns a reference can be
21      // used as an lvalue! POOR PROGRAMMING PRACTICE!!!!!!!!
22      t.badSetHour(12) = 74; // assign another invalid value to hour
23
24      cout << "After using t.badSetHour(12) as an lvalue, "
25           << fmt::format("hour is: {}
", t.getHour());
26   }
Valid hour before modification: 20
Invalid hour after modification: 30

After using t.badSetHour(12) as an lvalue, hour is: 74

Fig. 9.18 public member function that returns a reference to private data.

Images ERR Figure 9.18 declares Time object t (line 10) and reference hourRef (line 13), which we initialize with the reference returned by t.badSetHour(20). Line 15 displays hourRef’s value to show how hourRef breaks the class’s encapsulation. Statements in main should not have access to the private data in a Time object. Next, line 16 uses the hourRef to set hour’s value to 30 (an invalid value). Lines 17–18 call getHour to show that assigning to hourRef modifies t’s private data. Line 22 uses the badSetHour function call as an lvalue and assigns 74 (another invalid value) to the reference the function returns. Line 25 calls getHour again to show that line 22 modifies the private data in the Time object t.

Images SE Returning a reference or a pointer to a private data member breaks the class’s encapsulation, making the client code dependent on the class’s data representation. There are cases where doing this is appropriate. We’ll show an example of this when we build our custom Array class in Section 11.10.

9.15 Default Assignment Operator

The assignment operator (=) can assign an object to another object of the same type. The default assignment operator generated by the compiler copies each data member of the right operand into the same data member in the left operand. Figures 9.199.20 define a Date class. Line 15 of Fig. 9.21 uses the default assignment operator to assign Date object date1 to Date object date2. In this case, date1’s m_year, m_month and m_day members are assigned to date2’s m_year, m_month and m_day members, respectively.

 1   // Fig. 9.19: Date.h
 2   // Date class declaration. Member functions are defined in Date.cpp.
 3   #pragma once // prevent multiple inclusions of header
 4   #include <string>
 5
 6   // class Date definition
 7   class Date {
 8   public:
 9      explicit Date(int year, int month, int day);
10      std::string toString() const;
11   private:
12      int m_year;
13      int m_month;
14      int m_day;
15   };

Fig. 9.19 Date class declaration.

 1   // Fig. 9.20: Date.cpp
 2   // Date class member-function definitions.
 3   #include <string>
 4   #include <fmt/format.h> // In C++20, this will be #include <format>
 5   #include "Date.h" // include definition of class Date from Date.h
 6   using namespace std;
 7
 8   // Date constructor (should do range checking)
 9   Date::Date(int year, int month, int day)
10      : m_year{year}, m_month{month}, m_day{day} {}
11
12   // return string representation of a Date in the format yyyy-mm-dd
13   string Date::toString() const {
14      return fmt::format("{}-{:02d}-{:02d}", m_year, m_month, m_day);
15   }

Fig. 9.20 Date class member-function definitions.

 1   // fig09_21.cpp
 2   // Demonstrating that class objects can be assigned
 3   // to each other using the default assignment operator.
 4   #include <iostream>
 5   #include <fmt/format.h> // In C++20, this will be #include <format>
 6   #include "Date.h" // include definition of class Date from Date.h
 7   using namespace std;
 8
 9   int main() {
10      const Date date1{2004, 7, 4};
11      Date date2{2020, 1, 1};
12
13      cout << fmt::format("date1: {}
date2: {}

",
14                 date1.toString(), date2.toString());
15      date2 = date1; // uses the default assignment operator
16      cout << fmt::format("After assignment, date2: {}
", date2.toString());
17   }
date1: 2004-07-04
date2: 2020-01-01

After assignment, date2: 2004-07-04

Fig. 9.21 Class objects can be assigned to each other using the default assignment operator.

Copy Constructors

Objects may be passed as function arguments and may be returned from functions. Such passing and returning are performed using pass-by-value by default—a copy of the object is passed or returned. In such cases, C++ creates a new object and uses a copy constructor to copy the original object’s data into the new object. For each class we’ve shown so far, the compiler provides a default copy constructor that copies each member of the original object into the corresponding member of the new object.24

24. In Chapter 11, we’ll discuss cases in which the compiler uses move constructors, rather than copy constructors.

9.16 const Objects and const Member Functions

Let’s see how the principle of least privilege applies to objects. Some objects do not need to be modifiable, in which case you should declare them const. Any attempt to modify a const object results in a compilation error. The statement

const Time noon{12, 0, 0};

declares a const Time object noon and initializes it to 12 noon (12 PM). It’s possible to instantiate const and non-const objects of the same class.

Images PERF Declaring variables and objects const can improve performance. Compilers can perform optimizations on constants that cannot be performed on non-const variables.

Images SE C++ disallows calling a member function on a const object unless that member functions is declared const. So you should declared as const any member function that does not modify the object on which it’s called.

Images ERR A constructor must be allowed to modify an object to initialize it. A destructor must be allowed to perform its termination housekeeping before an object’s memory is reclaimed by the system. So, attempting to declare a constructor or destructor const is a compilation error. The “constness” of a const object is enforced throughout the object’s lifetime, from when the constructor finishes initializing the object until that object’s destructor is called.

Using const and Non-const Member Functions

The program of Fig. 9.22 uses a copy of class Time from Figs. 9.109.11, but removes const from function to12HourString’s prototype and definition to force a compilation error. We create two Time objects—non-const object wakeUp (line 6) and const object noon (line 7). The program attempts to invoke non-const member functions setHour (line 11) and to12HourString (line 15) on the const object noon. In each case, the compiler generates an error message. The program also illustrates the three other member-function-call combinations on objects:

• a non-const member function on a non-const object (line 10),

• a const member function on a non-const object (line 12) and

• a const member function on a const object (lines 13–14).

 1   // fig09_22.cpp
 2   // const objects and const member functions.
 3   #include "Time.h" // include Time class definition
 4
 5   int main() {
 6      Time wakeUp{6, 45, 0}; // non-constant object
 7      const Time noon{12, 0, 0}; // constant object
 8
 9                                // OBJECT      MEMBER FUNCTION
10      wakeUp.setHour(18);       // non-const   non-const
11      noon.setHour(12);         // const       non-const
12      wakeUp.getHour();         // non-const   const
13      noon.getMinute();         // const       const
14      noon.to24HourString();    // const       const
15      noon.to12HourString();    // const       non-const
16   }

Microsoft Visual C++ compiler error messages:

C:UsersPaulDeitelDocumentsexamplesch09fig09_22fig09_22.cpp(11,19): error C2662: 'void Time::setHour(int)': cannot convert 'this' pointer from 'const Time' to 'Time &'

C:UsersPaulDeitelDocumentsexamplesch09fig09_22fig09_22.cpp(11,4): message : Conversion loses qualifiers

C:UsersPaulDeitelDocumentsexamplesch09fig09_22Time.h(15,9): message : see declaration of 'Time::setHour'

C:UsersPaulDeitelDocumentsexamplesch09fig09_22fig09_22.cpp(15,26): error C2662: 'std::string Time::to12HourString(void)': cannot convert 'this' pointer from 'const Time' to 'Time &'

C:UsersPaulDeitelDocumentsexamplesch09fig09_22fig09_22.cpp(15,4): message : Conversion loses qualifiers

C:UsersPaulDeitelDocumentsexamplesch09fig09_22Time.h(25,16): message : see declaration of 'Time::to12HourString'

Fig. 9.22 const objects and const member functions.

The error messages generated for non-const member functions called on a const object are shown in the output window. We added blank lines for readability.

Images SE A constructor must be a non-const member function, but it can still be used to initialize a const object (Fig. 9.22, line 7). Recall from Fig. 9.11 that the Time constructor’s definition calls another non-const member function—setTime—to perform the initialization of a Time object. Invoking a non-const member function from the constructor call as part of the initialization of a const object is allowed. The object is not const until the constructor finishes initializing the object.

Line 15 in Fig. 9.22 generates a compilation error even though Time’s member function to12HourString does not modify the object on which it’s called. The fact that a member function does not modify an object is not sufficient. The function must explicitly be declared const for this call to be allowed by the compiler.

9.17 Composition: Objects as Members of Classes

Images SE An AlarmClock object needs to know when it’s supposed to sound its alarm, so why not include a Time object as a member of the AlarmClock class? Such a software-reuse capability is called composition (or aggregation) and is sometimes referred to as a has-a relation-ship—a class can have objects of other classes as members.25 You’ve already used composition in this chapter’s Account class examples. Class Account contained a string object as a data member.

25. As you’ll see in Chapter 10, classes also may be derived from other classes that provide attributes and behaviors the new classes can use—this is called inheritance.

You’ve seen how to pass arguments to the constructor of an object you created. Now we show how a class’s constructor can pass arguments to member-object constructors via member initializers.

The next program uses classes Date (Figs. 9.239.24) and Employee (Figs. 9.259.26) to demonstrate composition. Class Employee’s definition (Fig. 9.25) has private data members m_firstName, m_lastName, m_birthDate and m_hireDate. Members m_birth-Date and m_hireDate are const objects of class Date, which has private data members m_year, m_month and m_day. The Employee constructor’s prototype (Fig. 9.25, lines 11–12) specifies that the constructor has four parameters (firstName, lastName, birthDate and hireDate). The first two parameters are passed via member initializers to the string constructor for data members firstName and lastName. The last two are passed via member initializers to class Date’s constructor for data members birthDate and hireDate.

 1   // Fig. 9.23: Date.h
 2   // Date class definition; Member functions defined in Date.cpp
 3   #pragma once // prevent multiple inclusions of header
 4   #include <string>
 5
 6   class Date {
 7   public:
 8      static const int monthsPerYear{12}; // months in a year
 9      explicit Date(int year, int month, int day);
10      std::string toString() const; // date string in yyyy-mm-dd format
11      ~Date(); // provided to show when destruction occurs
12   private:
13      int m_year; // any year
14      int m_month; // 1-12 (January-December)
15      int m_day; // 1-31 based on month
16
17      // utility function to check if day is proper for month and year
18      bool checkDay(int day) const;
19   };

Fig. 9.23 Date class definition.

 1   // Fig. 9.24: Date.cpp
 2   // Date class member-function definitions.
 3   #include <array>
 4   #include <iostream>
 5   #include <stdexcept>
 6   #include <fmt/format.h> // In C++20, this will be #include <format>
 7   #include "Date.h" // include Date class definition
 8   using namespace std;
 9
10   // constructor confirms proper value for month; calls
11   // utility function checkDay to confirm proper value for day
12   Date::Date(int year, int month, int day)
13      : m_year{year}, m_month{month}, m_day{day} {
14      if (m_month < 1 || m_month > monthsPerYear) { // validate the month
15         throw invalid_argument{"month must be 1-12"};
16      }
17
18      if (!checkDay(day)) { // validate the day
19           throw invalid_argument{"Invalid day for current month and year"};
20      }
21
22      // output Date object to show when its constructor is called
23      cout << fmt::format("Date object constructor: {}
", toString());
24   }
25
26   // gets string representation of a Date in the form yyyy-mm-dd
27   string Date::toString() const {
28      return fmt::format("{}-{:02d}-{:02d}", m_year, m_month, m_day);
29   }
30
31   // output Date object to show when its destructor is called
32   Date::~Date() {
33      cout << fmt::format("Date object destructor: {}
", toString());
34   }
35
36   // utility function to confirm proper day value based on
37   // month and year; handles leap years, too
38   bool Date::checkDay(int day) const {
39      static const array daysPerMonth{
40         0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
41
42      // determine whether testDay is valid for specified month
43      if (1 <= day && day <= daysPerMonth.at(m_month)) {
44         return true;
45      }
46
47      // February 29 check for leap year
48      if (m_month == 2 && day == 29 && (m_year % 400 == 0 ||
49         (m_year % 4 == 0 && m_year % 100 != 0))) {
50         return true;
51      }
52
53      return false; // invalid day, based on current m_month and m_year
54   }

Fig. 9.24 Date class member-function definitions.

 1   // Fig. 9.25: Employee.h
 2   // Employee class definition showing composition.
 3   // Member functions defined in Employee.cpp.
 4   #pragma once // prevent multiple inclusions of header
 5   #include <string>
 6   #include <string_view>
 7   #include "Date.h" // include Date class definition
 8
 9   class Employee {
10   public:
11      Employee(std::string_view firstName, std::string_view lastName,
12         const Date& birthDate, const Date& hireDate);
13      std::string toString() const;
14      ~Employee(); // provided to confirm destruction order
15   private:
16      std::string m_firstName; // composition: member object
17      std::string m_lastName; // composition: member object
18      Date m_birthDate; // composition: member object
19      Date m_hireDate; // composition: member object
20   };

Fig. 9.25 Employee class definition showing composition.

 1   // Fig. 9.26: Employee.cpp
 2   // Employee class member-function definitions.
 3   #include <iostream>
 4   #include <fmt/format.h> // In C++20, this will be #include <format>
 5   #include "Employee.h" // Employee class definition
 6   #include "Date.h" // Date class definition
 7   using namespace std;
 8
 9   // constructor uses member initializer list to pass initializer
10   // values to constructors of member objects
11   Employee::Employee(string_view firstName, string_view lastName,
12      const Date &birthDate, const Date &hireDate)
13      : m_firstName{firstName}, m_lastName{lastName},
14        m_birthDate{birthDate}, m_hireDate{hireDate} {
15      // output Employee object to show when constructor is called
16      cout << fmt::format("Employee object constructor: {} {}
",
17                 m_firstName, m_lastName);
18   }
19
20   // gets string representation of an Employee object
21   string Employee::toString() const {
22      return fmt::format("{}, {}  Hired: {}  Birthday: {}", m_lastName,
23                m_firstName, m_hireDate.toString(), m_birthDate.toString());
24   }
25
26   // output Employee object to show when its destructor is called
27   Employee::~Employee() {
28      cout << fmt::format("Employee object destructor: {}, {}
",
29                 m_lastName, m_firstName);
30   }

Fig. 9.26 Employee class member-function definitions.

Employee Constructor’s Member-Initializer List

Images SE Images CG The colon (:) following the Employee constructor’s header (Fig. 9.26, line 13) begins the member-initializer list. The member initializers pass the constructor’s parameters first-Name, lastName, birthDate and hireDate to the constructors of the composed string and Date data members m_firstName, m_lastName, m_birthDate and m_hireDate, respectively. The order of the member initializers does not matter. Data members are constructed in the order that they’re declared in class Employee, not in the order they appear in the member-initializer list. For clarity, the C++ Core Guidelines recommend listing the member initializers in the order they’re declared in the class.26

26. C++ Core Guidelines. Accessed July 12, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-order.

Date Class’s Default Copy Constructor

As you study class Date (Fig. 9.23), notice it does not provide a constructor with a Date parameter. So, why can the Employee constructor’s member-initializer list initialize the m_birthDate and m_hireDate objects by passing Date objects to their constructors? As we mentioned in Section 9.15, the compiler provides each class with a default copy constructor that copies each data member of the constructor’s argument object into the corresponding member of the object being initialized. Chapter 11 discusses how to define customized copy constructors.

Testing Classes Date and Employee

Figure 9.27 creates two Date objects (lines 10–11) and passes them as arguments to the constructor of the Employee object created in line 12. There are fivetotal constructor calls when an Employee is constructed:

• two calls to the string class’s constructor (line 13 of Fig. 9.26),

• two calls to the Date class’s default copy constructor (line 14 of Fig. 9.26),

• and the call to the Employee class’s constructor, which calls the other four.

Line 14 of Fig. 9.27 outputs the Employee object’s data.

 1   // fig09_27.cpp
 2   // Demonstrating composition--an object with member objects.
 3   #include <iostream>
 4   #include <fmt/format.h> // In C++20, this will be #include <format>
 5   #include "Date.h" // Date class definition
 6   #include "Employee.h" // Employee class definition
 7   using namespace std;
 8
 9   int main() {
10      const Date birth{1987 ,7, 24};
11      const Date hire{2018, 3, 12};
12      const Employee manager{"Sue", "Green", birth, hire};
13
14      cout << fmt::format("
{}
", manager.toString());
15   }
Date object constructor: 1987-07-24
Date object constructor: 2018-03-12
Employee object constructor: Sue Green

Green, Sue  Hired: 2018-03-12  Birthday: 1987-07-24
Employee object destructor: Green, Sue
Date object destructor: 2018-03-12
Date object destructor: 1987-07-24
Date object destructor: 2018-03-12
Date object destructor: 1987-07-24

Fig. 9.27 Demonstrating composition—an object with member objects.

When each Date object is created in lines 10–11, the Date constructor (lines 12–24 of Fig. 9.24) displays a line of output to show that the constructor was called (see the first two lines of the sample output). However, line 12 of Fig. 9.27 causes two Date copy-constructor calls (line 14 of Fig. 9.26) that do not appear in this program’s output. Since the compiler defines our Date class’s copy constructor, it does not contain any output statements to demonstrate when it’s called.

Class Date and class Employee each include a destructor (lines 32–34 of Fig. 9.24 and lines 27–30 of Fig. 9.26, respectively) that prints a message when an object of its class is destructed. The destructors help us show that, though objects are constructed from the inside out, they’re destructed from the outside in. That is, the Date member objects are destructed after the enclosing Employee object.

Notice the last four lines in the output of Fig. 9.27. The last two lines are the outputs of the Date destructor running on Date objects hire (Fig. 9.27, line 11) and birth (line 10), respectively. The outputs confirm that the three objects created in main are destructed in the reverse of the order from which they were constructed. The Employee destructor output is five lines from the bottom. The fourth and third lines from the bottom of the output show the destructors running for the Employee’s member objects m_hireDate (Fig. 9.25, line 19) and m_birthDate (line 18).

These outputs confirm that the Employee object is destructed from the outside in. The Employee destructor runs first (see the output five lines from the bottom). Then the member objects are destructed in the reverse order from which they were constructed. Class string’s destructor does not contain output statements, so we do not see the first-Name and lastName objects being destructed.

What Happens When You Do Not Use the Member-Initializer List?

Images ERR If you do not initialize a member object explicitly, the member object’s default constructor will be called implicitly to initialize the member object. If there is no default constructor, a compilation error occurs. Values set by the default constructor can be changed later by set functions. However, for complex initialization, this approach may require significant additional work and time.

Images PERF Initializing member objects explicitly through member initializers eliminates the overhead of “doubly initializing” member objects—once when the member object’s default constructor is called and again when set functions are called in the constructor body (or later) to change values in the member object.

9.18 friend Functions and friend Classes

A friend function is a function with access to a class’s public and non-public class members. A class may have as friends:

• standalone functions,

• entire classes (and thus all their functions) or

• specific member functions of other classes.

This section presents a mechanical example of how a friend function works. In Chapter 11, Operator Overloading, we’ll show friend functions that overload operators for use with objects of custom classes. You’ll see that sometimes a member function cannot be used to define certain overloaded operators.

Declaring a friend

To declare a non-member function as a friend of a class, place the function prototype in the class definition and precede it with the keyword friend. To declare all member functions of class ClassTwo as friends of class ClassOne, place in ClassOne’s definition a declaration of the form:

friend class ClassTwo;
Friendship Rules

Images SE These are the basic friendship rules:

• Friendship is granted, not taken—For class B to be a friend of class A, class A must declare that class B is its friend.

• Friendship is not symmetric—If class A is a friend of class B, you cannot infer that class B is a friend of class A.

• Friendship is not transitive—If class A is a friend of class B and class B is a friend of class C, you cannot infer that class A is a friend of class C.

friends Are Not Subject to Access Modifiers

Member access notions of public, protected (Chapter 10) and private do not apply to friend declarations, so friend declarations can be placed anywhere in a class definition. We prefer to place all friendship declarations first inside the class definition’s body and not precede them with any access specifier.

Modifying a Class’s private Data with a friend Function

Figure 9.28 defines friend function setX to set class Count’s private data member m_x. Though a friend declaration can appear anywhere in the class, by convention, place the friend declaration (line 9) first in the class definition, .

 1   // fig09_28.cpp
 2   // Friends can access private members of a class.
 3   #include <iostream>
 4   #include <fmt/format.h> // In C++20, this will be #include <format>
 5   using namespace std;
 6
 7   // Count class definition
 8   class Count {
 9      friend void setX(Count& c, int value); // friend declaration
10   public:
11      int getX() const {return m_x;}
12   private:
13      int m_x{0};
14   };
15
16   // function setX can modify private data of Count
17   // because setX is declared as a friend of Count (line 8)
18   void setX(Count& c, int value) {
19      c.m_x = value; // allowed because setX is a friend of Count
20   }
21
22   int main() {
23      Count counter{}; // create Count object
24
25      cout << fmt::format("Initial counter.x value: {}
", counter.getX());
26      setX(counter, 8); // set x using a friend function
27      cout << fmt::format("counter.x after setX call: {}
", counter.getX());
28   }
counter.x after instantiation: 0
counter.x after call to setX friend function: 8

Fig. 9.28 Friends can access private members of a class.

Function setX (lines 18–20) is a standalone (free) function, not a Count member function. So, when we call setX to modify counter (line 26), we must pass counter as an argument to setX. Function setX is allowed to access class Count’s private data member m_x (line 19) only because the function was declared as a friend of class Count (line 9). If you remove the friend declaration, you’ll receive error messages indicating that function setX cannot modify class Count’s private data member m_x.

Overloaded friend Functions

It’s possible to specify overloaded functions as friends of a class. Each overload intended to be a friend must be explicitly declared in the class definition as a friend, as you’ll see in Chapter 11.

9.19 The this Pointer

There’s only one copy of each class’s functionality, but there can be many objects of a class, so how do member functions know which object’s data members to manipulate? Every object’s member functions access the object through a pointer called this (a C++ keyword), which is an implicit argument to each of the object’s non-static27 member functions.

27. Section 9.20 introduces static class members and explains why the this pointer is not implicitly passed to static member functions.

Using the this Pointer to Avoid Naming Collisions

Images SE Member functions use the this pointer implicitly (as we’ve done so far) or explicitly to reference an object’s data members and other member functions. One explicit use of the this pointer is to avoid naming conflicts between a class’s data members and constructor or member-function parameters. If a member function uses a local variable and data member with the same name, the local variable is said to hide or shadow the data member. Using just the variable name in the member function’s body refers to the local variable rather than the data member.

You can access the data member explicitly by qualifying its name with this->. For instance, we could implement class Time’s setHour function as follows:

// set hour value
void Time::setHour(int hour) {
   if (hour < 0 || hour >= 24) {
      throw invalid_argument{"hour must be 0-23"};
   }

   this->hour = hour; // use this-> to access data member
}

where this->hour represents a data member called hour. You can avoid such naming collisions by naming your data members with the "m_" prefix, as shown in our classes so far.

Type of the this Pointer

The this pointer’s type depends on the object’s type and whether the member function in which this is used is declared const:

• In a non-const member function of class Time, the this pointer is a Time*—a pointer to a Time object.

• In a const member function, this is a const Time*—a pointer to a Time constant.

9.19.1 Implicitly and Explicitly Using the this Pointer to Access an Object’s Data Members

Figure 9.29 is a mechanical example that demonstrates implicit and explicit use of the this pointer in a member function to display the private data m_x of a Test object. In Section 9.19.2 and in Chapter 11, we show some substantial and subtle examples of using this.

 1   // fig09_29.cpp
 2   // Using the this pointer to refer to object members.
 3   #include <iostream>
 4   #include <fmt/format.h> // In C++20, this will be #include <format>
 5   using namespace std;
 6
 7   class Test {
 8   public:
 9      explicit Test(int value);
10      void print() const;
11   private:
12      int m_x{0};
13   };
14
15   // constructor
16   Test::Test(int value) : m_x{value} {} // initialize x to value
17
18   // print x using implicit then explicit this pointers;
19   // the parentheses around *this are required due to precedence
20   void Test::print() const {
21      // implicitly use the this pointer to access the member x
22      cout << fmt::format("  x = {}
", m_x);
23
24      // explicitly use the this pointer and the arrow operator
25      // to access the member x
26      cout << fmt::format("  this->x = {}
", this->m_x);
27
28      // explicitly use the dereferenced this pointer and
29      // the dot operator to access the member x
30      cout << fmt::format("(*this).x = {}
", (*this).m_x);
31   }
32
33   int main() {
34      const Test testObject{12}; // instantiate and initialize testObject
35      testObject.print();
36   }
        x = 12
  this->x = 12
(*this).x = 12

Fig. 9.29 Using the this pointer to refer to object members.

For illustration purposes, member function print (lines 20–31) first displays x using the this pointer implicitly (line 22)—only the data member’s name is specified. Then print uses two different notations to access x through the this pointer:

this->m_x (line 26) and

(*this).m_x (line 30).

The parentheses around *this (line 30) are required because the dot operator (.) has higher precedence than the * pointer-dereferencing operator. Without the parentheses, the expression *this.x would be evaluated as if it were parenthesized as *(this.x). The dot operator cannot be used with a pointer, so this would be a compilation error.

9.19.2 Using the this Pointer to Enable Cascaded Function Calls

Images SE Another use of the this pointer is to enable cascaded member-function calls—that is, invoking multiple functions sequentially in the same statement, as you’ll see in line 11 of Fig. 9.32. The program of Figs. 9.309.32 modifies class Time’s setTime, setHour, set-Minute and setSecond functions such that each returns a reference to the Time object on which it’s called. This reference enables cascaded member-function calls. In Fig. 9.31, the last statement in each of these member functions’ bodies returns a reference to *this (lines 19, 29, 39 and 49).

 1   // Fig. 9.30: Time.h
 2   // Time class modified to enable cascaded member-function calls.
 3   #pragma once // prevent multiple inclusions of header
 4   #include <string>
 5
 6   class Time {
 7   public:
 8      // default constructor because it can be called with no arguments
 9      explicit Time(int hour = 0, int minute = 0, int second = 0);
10
11      // set functions
12      Time& setTime(int hour, int minute, int second);
13      Time& setHour(int hour); // set hour (after validation)
14      Time& setMinute(int minute); // set minute (after validation)
15      Time& setSecond(int second); // set second (after validation)
16
17      int getHour() const; // return hour
18      int getMinute() const; // return minute
19      int getSecond() const; // return second
20      std::string to24HourString() const; // 24-hour time format string
21      std::string to12HourString() const; // 12-hour time format string
22   private:
23      int m_hour{0}; // 0 - 23 (24-hour clock format)
24      int m_minute{0}; // 0 - 59
25      int m_second{0}; // 0 - 59
26   };

Fig. 9.30 Time class modified to enable cascaded member-function calls.

 1   // Fig. 9.31: Time.cpp
 2   // Time class member-function definitions.
 3   #include <stdexcept>
 4   #include <fmt/format.h> // In C++20, this will be #include <format>
 5   #include "Time.h" // Time class definition
 6   using namespace std;
 7
 8   // Time constructor initializes each data member
 9   Time::Time(int hour, int minute, int second) {
10      setHour(hour); // validate and set private field m_hour
11      setMinute(minute); // validate and set private field m_minute
12      setSecond(second); // validate and set private field m_second
13   }
14
15   // set new Time value using 24-hour time
16   Time& Time::setTime(int hour, int minute, int second) {
17      Time time{hour, minute, second}; // create a temporary Time object
18      *this = time; // if time is valid, assign its members to current object
19      return *this; // enables cascading
20   }
21
22   // set hour value
23   Time& Time::setHour(int hour) { // note Time& return
24      if (hour < 0 || hour >= 24) {
25         throw invalid_argument{"hour must be 0-23"};
26      }
27
28      m_hour = hour;
29      return *this; // enables cascading
30   }
31
32   // set minute value
33   Time& Time::setMinute(int m) { // note Time& return
34      if (m < 0 || m >= 60) {
35         throw invalid_argument{"minute must be 0-59"};
36      }
37
38      m_minute = m;
39      return *this; // enables cascading
40   }
41
42   // set second value
43   Time& Time::setSecond(int s) { // note Time& return
44      if (s < 0 || s >= 60) {
45         throw invalid_argument{"second must be 0-59"};
46      }
47
48      m_second = s;
49      return *this; // enables cascading
50   }
51
52   // get hour value
53   int Time::getHour() const {return m_hour;}
54
55   // get minute value
56   int Time::getMinute() const {return m_minute;}
57
58   // get second value
59   int Time::getSecond() const {return m_second;}
60
61   // return Time as a string in 24-hour format (HH:MM:SS)
62   string Time::to24HourString() const {
63      return fmt::format("{:02d}:{:02d}:{:02d}",
64                getHour(), getMinute(), getSecond());
65   }
66
67   // return Time as string in 12-hour format (HH:MM:SS AM or PM)
68   string Time::to12HourString() const {
69      return fmt::format("{}:{:02d}:{:02d} {}",
70         ((getHour() % 12 == 0) ? 12 : getHour() % 12),
71         getMinute(), getSecond(), (getHour() < 12 ? "AM" : "PM"));
72   }

Fig. 9.31 Time class member-function definitions modified to enable cascaded member-function calls.

 1   // fig09_32.cpp
 2   // Cascading member-function calls with the this pointer.
 3   #include <iostream>
 4   #include <fmt/format.h> // In C++20, this will be #include <format>
 5   #include "Time.h" // Time class definition
 6   using namespace std;
 7
 8   int main() {
 9      Time t{}; // create Time object
10
11      t.setHour(18).setMinute(30).setSecond(22); // cascaded function calls
12
13       // output time in 24-hour and 12-hour formats
14      cout << fmt::format("24-hour time: {}
12-hour time: {}

",
15                 t.to24HourString(), t.to12HourString());
16
17      // cascaded function calls
18      cout << fmt::format("New 12-hour time: {}
",
19                 t.setTime(20, 20, 20).to12HourString());
20   }
24-hour time: 18:30:22
12-hour time: 6:30:22 PM
New 12-hour time: 8:20:20 PM

Fig. 9.32 Cascading member-function calls with the this pointer.

Notice the elegant new implementation of member function setTime. Line 17 in Fig. 9.31, creates a local Time object called time using setTime’s arguments. While initializing this object, the constructor call will fail and throw an exception if any argument is out of range. Otherwise, setTime’s arguments are all valid, so line 18 assigns the time object’s members to *this—the Time object on which setTime was called.

In the next program (Fig. 9.32), we create Time object t (line 9), then use it in cascaded member-function calls (lines 11 and 19).

Why does the technique of returning *this as a reference work? The dot operator (.) groups left-to-right, so line 11

t.setHour(18).setMinute(30).setSecond(22);

first evaluates t.setHour(18), which returns a reference to (the updated) object t as the value of this function call. The remaining expression is then interpreted as

t.setMinute(30).setSecond(22);

The t.setMinute(30) call executes and returns a reference to the (further updated) object t. The remaining expression is interpreted as

t.setSecond(22);

Line 19 (Fig. 9.32) also uses cascading. Note that we cannot chain another Time member-function call after to12HourString, because it does not return a reference to a Time object. However, we could chain a call to a string member function, because to12Hour-String returns a string. Chapter 11 presents several practical examples of using cascaded function calls.

9.20 static Class Members—Classwide Data and Member Functions

There is an important exception to the rule that each object of a class has its own copy of all the class’s data members. In certain cases, all objects of a class should share only one copy of a variable. A static data member is used for these and other reasons. Such a variable represents “classwide” information—that is, data shared by all objects of the class. You can use static data members to save storage when a single copy of the data for all objects of a class will suffice, such as a constant that can be shared by all objects of the class.

Motivating Classwide Data

Let’s further motivate the need for static classwide data with an example. Suppose that we have a video game with Martians and other space creatures. Each Martian tends to be brave and willing to attack other space creatures when the Martian is aware that at least five Martians are present. If fewer than five are present, each Martian becomes cowardly. So each Martian needs to know the martianCount. We could endow each object of class Martian with martianCount as a data member. If we do, every Martian will have its own copy of the data member. Every time we create a new Martian, we’d have to update the data member martianCount in all Martian objects. Doing this would require every Martian object to know about all other Martian objects in memory. This wastes space with redundant martianCount copies and wastes time in updating the separate copies. Instead, we declare martianCount to be static to make it classwide data. Every Martian can access martianCount as if it were a data member of the Martian, but only one copy of the static variable martianCount is maintained in the program. This saves space. We havve the Martian constructor increment static variable martianCount and the Martian destructor decrement martianCount. Because there’s only one copy, we do not have to increment or decrement separate copies of martianCount for every Martian object.

Scope and Initialization of static Data Members

17 A class’s static data members have class scope. A static data member must be initialized exactly once. Fundamental-type static data members are initialized by default to 0. A static const data member can have an in-class initializer. As of C++17, you also may use in-class initializers for a non-const static data member by preceding its declaration with the inline keyword (as you’ll see momentarily in Fig. 9.33). If a static data member is an object of a class that provides a default constructor, the static data member need not be explicitly initialized because its default constructor will be called.

 1   // Fig. 9.33: Employee.h
 2   // Employee class definition with a static data member to
 3   // track the number of Employee objects in memory
 4   #pragma once
 5   #include <string>
 6   #include <string_view>
 7
 8   class Employee {
 9   public:
10      Employee(std::string_view firstName, std::string_view lastName);
11      ~Employee(); // destructor
12      const std::string& getFirstName() const; // return first name
13      const std::string& getLastName() const; // return last name
14
15      // static member function
16      static int getCount(); // return # of objects instantiated
17   private:
18      std::string m_firstName;
19      std::string m_lastName;
20
21      // static data
22      inline static int m_count{0}; // number of objects instantiated
23   };

Fig. 9.33 Employee class definition with a static data member to track the number of Employee objects in memory.

Accessing static Data Members

A class’s static members exist even when no objects of that class exist. To access a public static class data member or member function, simply prefix the class name and the scope resolution operator (::) to the member name. For example, if our martianCount variable is public, it can be accessed with Martian::martianCount, even when there are no Martian objects. (Of course, using public data is discouraged.)

Images SE A class’s private (and protected; Chapter 10) static members are normally accessed through the class’s public member functions or friends. To access a private static or protected static data member when no objects of the class exist, provide a public static member function and call the function by prefixing its name with the class name and scope resolution operator. A static member function is a service of the class as a whole, not of a specific object of the class.

Demonstrating static Data Members

This example demonstrates a private inline static data member called m_count (Fig. 9.33, line 22), which is initialized to 0, and a public static member function called getCount (Fig. 9.33, line 16). A static data member also can be initialized at file scope in the class’s implementation file. For instance, we could have placed the following statement in Employee.cpp (Fig. 9.34) after the Employee.h header is included:

int Employee::count{0};
 1   // Fig. 9.34: Employee.cpp
 2   // Employee class member-function definitions.
 3   #include <iostream>
 4   #include <fmt/format.h> // In C++20, this will be #include <format>
 5   #include "Employee.h" // Employee class definition
 6   using namespace std;
 7
 8   // define static member function that returns number of
 9   // Employee objects instantiated (declared static in Employee.h)
10   int Employee::getCount() {return m_count;}
11
12   // constructor initializes non-static data members and
13   // increments static data member count
14   Employee::Employee(string_view firstName, string_view lastName)
15      : m_firstName(firstName), m_lastName(lastName) {
16      ++m_count; // increment static count of employees
17      cout << fmt::format("Employee constructor called for {} {}
",
18                 m_firstName, m_lastName);
19   }
20
21   // destructor decrements the count
22   Employee::~Employee() {
23      cout << fmt::format("~Employee() called for {} {}
",
24                 m_firstName, m_lastName);
25      --m_count; // decrement static count of employees
26   }
27
28   // return first name of employee
29   const string& Employee::getFirstName() const {return m_firstName;}
30
31   // return last name of employee
32   const string& Employee::getLastName() const {return m_lastName;}

Fig. 9.34 Employee class member-function definitions.

In Fig. 9.34, line 10 defines static member function getCount. Note that line 10 does not include the static keyword, which cannot be applied to a member definition that appears outside the class definition. In this program, data member m_count maintains a count of the number of Employee objects in memory at a given time. When Employee objects exist, member m_count can be referenced through any member function of an Employee object. In Fig. 9.34, m_count is referenced by both line 16 in the constructor and line 25 in the destructor.

Figure 9.35 uses static member function getCount to determine the number of Employee objects in memory at various points in the program. The program calls Employee::getCount():

• before any Employee objects have been created (line 12),

• after two Employee objects have been created (line 23) and

• after those Employee objects have been destroyed (line 33).

 1   // fig09_35.cpp
 2   // static data member tracking the number of objects of a class.
 3   #include <iostream>
 4   #include <fmt/format.h> // In C++20, this will be #include <format>
 5   #include "Employee.h" // Employee class definition
 6   using namespace std;
 7
 8   int main() {
 9      // no objects exist; use class name and scope resolution
10      // operator to access static member function getCount
11      cout << fmt::format("Initial employee count: {}
",
12                 Employee::getCount()); // use class name
13
14      // the following scope creates and destroys
15      // Employee objects before main terminates
16      {
17         const Employee e1{"Susan", "Baker"};
18         const Employee e2{"Robert", "Jones"};
19
20         // two objects exist; call static member function getCount again
21         // using the class name and the scope resolution operator
22         cout << fmt::format("Employee count after creating objects: {}

",
23                    Employee::getCount());
24
25         cout << fmt::format("Employee 1: {} {}
Employee 2: {} {}

",
26                    e1.getFirstName(), e1.getLastName(),
27                    e2.getFirstName(), e2.getLastName());
28      }
29
30      // no objects exist, so call static member function getCount again
31      // using the class name and the scope resolution operator
32      cout << fmt::format("Employee count after objects are deleted: {}
",
33                 Employee::getCount());
34   }
Initial employee count: 0
Employee constructor called for Susan Baker
Employee constructor called for Robert Jones
Employee count after creating objects: 2

Employee 1: Susan Baker
Employee 2: Robert Jones
~Employee() called for Robert Jones
~Employee() called for Susan Baker
Employee count after objects are deleted: 0

Fig. 9.35 static data member tracking the number of objects of a class.

Lines 16–28 in main define a nested scope. Recall that local variables exist until the scope in which they’re defined terminates. In this example, we create two Employee objects in the nested scope (lines 17–18). As each constructor executes, it increments class Employee’s static data member count. These Employee objects are destroyed when the program reaches line 28. At that point, each object’s destructor executes and decrements class Employee’s static data member count.

static Member Function Notes

Images SE A member function should be declared static if it does not access the class’s non-static data members or non-static member functions. A static member function does not have a this pointer, because static data members and static member functions exist independently of any objects of a class. The this pointer must refer to a specific object, but a static member function can be called when there are no objects of its class in memory. So, using the this pointer in a static member function is a compilation error.

Images ERR Images ERR A static member function may not be declared const. The const qualifier indicates that a function cannot modify the contents of the object on which it operates, but static member functions exist and operate independently of any objects of the class. So, declaring a static member function const is a compilation error.

20 9.21 Aggregates in C++20

According to Section 9.4.1 of the C++ standard document

http://wg21.link/n4861

an aggregate type is a built-in array, an array object or an object of a class that:

• does not have user-declared constructors,

• does not have private or protected (Chapter 10) non-static data members,

• does not have virtual functions (Chapter 10) and

• does not have virtual (Chapter 19), private (Chapter 10) or protected (Chapter 10) base classes.

20 The requirement for no user-declared constructors was a C++20 change to the definition of aggregates. It prevents a case in which initializing an aggregate object could circumvent calling a user-declared constructor.28

28. “Prohibit aggregates with user-declared constructors.” Accessed July 12, 2020. http://wg21.link/p1008r1.

You can define an aggregate using a class in which all the data is declared public. However, a struct is a class that contains only public members by default. The following struct defines an aggregate type named Record containing four public data members:

struct Record {
   int account;
   string first;
   string last;
   double balance;
};

Images CG The C++ Core Guidelines recommend using class rather than struct if any data member or member function needs to be non-public.29

29. C++ Core Guidelines. Accessed July 12. 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-class.

9.21.1 Initializing an Aggregate

You can initialize an object of aggregate type Record as follows:

Record record{100, "Brian", "Blue", 123.45};

11 In C++11, you could not use a list initializer for an aggregate-type object if any of the type’s non-static data-member declarations contained in-class initializers. For example, the initialization above would have generated a compilation error if the aggregate type Record were defined with a default value for balance, as in:

struct Record {
   int account;
   string first;
   string last;
   double balance{0.0};
};

14 C++14 removed this restriction. Also, if you initialize an aggregate-type object with fewer initializers than there are data members in the object, as in

Record record{0, "Brian", "Blue"};

the remaining data members are initialized as follows:

• Data members with in-class initializers use those values—in the preceding case, record’s balance is set to 0.0.

• Data members without in-class initializers are initialized with empty braces ({}). Empty-brace initialization sets fundamental-type variables to 0, sets bools to false and value initializes objects—that is, they’re zero initialized, then the default constructor is called for each object.

20 9.21.2 C++20: Designated Initializers

As of C++20, aggregates now support designated initializers in which you can specify which data members to initialize by name. Using the preceding struct Record definition, we could initialize a Record object as follows:

Record record{.first{"Sue"}, .last{"Green"}};

explicitly initializing only a subset of the data members. Each explicitly named data member is preceded by a dot (.), and the identifiers that you specify must be listed in the same order as they’re declared in the aggregate type. The preceding statement initializes the data members first and last to "Sue" and "Green", respectively. The remaining data members get their default initializer values:

account is set to 0 and

balance is set to its default value in the type definition—in this case, 0.0.

Other Benefits of Designated Initializers30

Images SE Adding new data members to an aggregate type will not break existing statements that use designated initializers. Any new data members that are not explicitly initialized simply receive their default initialization. Designated initializers also improve compatibility with the C programming language, which has had this feature since C99.

30. “Designated Initialization.” Accessed July 12, 2020. http://wg21.link/p0329r0.

9.22 Objects Natural Case Study: Serialization with JSON

More and more computing today is done “in the cloud”—that is, distributed across the Internet. Many applications you use daily communicate over the Internet with cloud-based services that use massive clusters of computing resources (computers, processors, memory, disk drives, databases, etc.).

A service that provides access to itself over the Internet is known as a web service. Applications typically communicate with web services by sending and receiving JSON objects. JSON (JavaScript Object Notation) is a text-based, human-and-computer-readable, data-interchange format that represents objects as collections of name–value pairs. JSON has become the preferred data format for transmitting objects across platforms.

JSON Data Format

Each JSON object contains a comma-separated list of property names and values in curly braces. For example, the following name–value pairs might represent a client record:

{"account": 100, "name": "Jones", "balance": 24.98}

JSON also supports arrays as comma-separated values in square brackets. For example, the following represents a JSON array of numbers:

[100, 200, 300]

Values in JSON objects and arrays can be:

• strings in double-quotes (like "Jones"),

• numbers (like 100 or 24.98),

• JSON Boolean values (represented as true or false),

null (to represent no value),

• arrays of any valid JSON value, and

• other JSON objects.

JSON arrays may contain elements of the same or different types.

Serialization

Converting an object into another format for storage or transmission over the Internet is known as serialization. Similarly, reconstructing an object from serialized data is known as deserialization. JSON is just one of several serialization formats. Other common formats include binary data and XML (eXtensible Markup Language).

Images SEC Serialization Security

Some programming languages have their own serialization mechanisms that use a language-native format. Deserializing objects using these native serialization formats is a source of various security issues. According to the Open Web Application Security Project (OWASP), these native mechanisms “can be repurposed for malicious effect when operating on untrusted data. Attacks against deserializers have been found to allow denial-of-service, access control, and remote code execution (RCE) attacks.”31 OWASP also indicates that you can significantly reduce attack risk by avoiding language-native serialization formats in favor of “pure data” formats like JSON or XML.

31. “Deserialization Cheat Sheet.” OWASP Cheat Sheet Series. Accessed July 18, 2020. https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html.

cereal Header-Only Serialization Library

The cereal header-only library32 serializes objects to and deserializes objects from JSON (which we’ll demonstrate), XML or binary formats. The library supports fundamental types and can handle most standard library types if you include each type’s appropriate cereal header. As you’ll see in the next section, cereal also supports custom types. The cereal documentation is available at:

https://uscilab.github.io/cereal/index.html

32. Copyright (c) 2017, Grant, W. Shane and Voorhies, Randolph. cereal—A C++11 library for serialization. URL: http://uscilab.github.io/cereal/. All rights reserved.

We’ve included the cereal library for your convenience in the libraries folder with the book’s examples. You must point your IDE or compiler at the library’s include folder, as you’ve done in several earlier Objects Natural case studies.

9.22.1 Serializing a vector of Objects containing public Data

Let’s begin by serializing objects containing only public data. In Fig. 9.36, we:

• create a vector of Record objects and display its contents,

• use cereal to serialize it to a text file, then

• deserialize the file’s contents into a vector of Record objects and display the deserialized Records.

 1   // fig09_36.cpp
 2   // Serializing and deserializing objects with the cereal library.
 3   #include <iostream>
 4   #include <fstream>
 5   #include <vector>
 6   #include <fmt/format.h> // In C++20, this will be #include <format>
 7   #include <cereal/archives/json.hpp>
 8   #include <cereal/types/vector.hpp>
 9
10   using namespace std;

Fig. 9.36 Serializing and deserializing objects with the cereal library.

To perform JSON serialization, include the cereal header json.hpp (line 7). To serialize a std::vector, include the cereal header vector.hpp (line 8).

Aggregate Type Record

Record is an aggregate type defined as a struct. Recall that an aggregate’s data must be public, which is the default for a struct definition:

11
12   struct Record {
13      int account{};
14      string first{};
15      string last{};
16      double balance{};
17   };
18

Fig. 9.37

Function serialize for Record Objects

The cereal library allows you to designate how to perform serialization several ways. If the types you wish to serialize have all public data, you can simply define a function template serialize (lines 21–27) that receives an Archive as its first parameter and an object of your type as the second.33 This function is called both for serializing and deserializing the Record objects. Using a function template enables you to choose among serialization and deserialization using JSON, XML or binary formats by passing an object of the appropriate cereal archive type. The library provides archive implementations for each case.

33. Function serialize also may be defined as a member function template with only an Archive parameter. For details, see https://uscilab.github.io/cereal/serialization_functions.html.

19   // function template serialize is responsible for serializing and
20   // deserializing Record objects to/from the specified Archive
21   template <typename Archive>
22   void serialize(Archive& archive, Record& record) {
23      archive(cereal::make_nvp("account", record.account),
24         cereal::make_nvp("first", record.first),
25         cereal::make_nvp("last", record.last),
26         cereal::make_nvp("balance", record.balance));
27   }
28

Each cereal archive type has an overloaded parentheses operator that enables you to use parameter archive objects as function names, as shown with parameter archive in lines 23–26. Depending on whether you’re serializing or deserializing a Record, this function will either:

• output the contents of the Record to a specified stream or

• input previously serialized data from a specified stream and create a Record object.

Each call to cereal::make_nvp (that is, “make name–value pair”), like line 23

cereal::make_nvp("account", record.account)

is primarily for the serialization step. It makes a name–value pair with the name in the first argument (in this case, "account") and the value in the second argument (in this case, the int value record.account). Naming the values is not required but makes the JSON output more readable as you’ll soon see. Otherwise, cereal uses names like value0, value1, etc.

Function displayRecords

We provide function displayRecords to show you the contents of our Record objects before serialization and after deserialization. The function simply displays the contents of each Record in the vector it receives as an argument:

29   // display record at command line
30   void displayRecords(const vector<Record>& records) {
31      for (const auto& r : records) {
32         cout << fmt::format("{} {} {} {:.2f}
",
33                    r.account, r.first, r.last, r.balance);
34      }
35   }
36
Creating Record Objects to Serialize

Lines 38–41 in main create a vector and initialize it with two Records (lines 39 and 40). The compiler determines the vector’s Record element type from the initializers. Line 44 outputs the vector’s contents to confirm that the two Records were initialized properly:

37   int main() {
38      vector records{
39         Record{100, "Brian", "Blue", 123.45},
40         Record{200, "Sue", "Green", 987.65}
41      };
42
43      cout << "Records to serialize:
";
44      displayRecords(records);
45
Records to serialize:
100 Brian Blue 123.45
200 Sue Green 987.65
Serializing Record Objects with cereal::JSONOutputArchive

A cereal::JSONOutputArchive serializes data in JSON format to a specified stream, such as the standard output stream or a stream representing a file. Line 47 attempts to open the file records.json for writing. If successful, line 48 creates a cereal::JSONOutputArchive object named archive and initializes it with the output ofstream object so archive can write the JSON data into a file. Line 49 uses the archive object to output a name–value pair with the name "records" and the vector of Records as its value. Part of serializing the vector is serializing each of its elements. So line 49 also results in one call to serialize (lines 21–27) for each Record object in the vector.

46      // serialize vector of Records to JSON and store in text file
47      if (ofstream output{"records.json"}) {
48         cereal::JSONOutputArchive archive{output};
49         archive(cereal::make_nvp("records", records)); // serialize records
50      }
51
Contents of records.json

After line 49 executes, the file records.json contains the following JSON data:

Images

The outer braces represent the entire JSON document. The darker box highlights the document’s one name–value pair named "records", which has as its value a JSON array containing the two JSON objects in the lighter boxes. The JSON array represents the vector records that we serialized in line 49. Each of the JSON objects in the array contains one Record’s four name–value pairs that were serialized by the serialize function.

Deserializing Record Objects with cereal::JSONInputArchive

Next, let’s deserialize the data and use it to fill a separate vector of Record objects. For cereal to recreate objects in memory, it must have access to each type’s default constructor. It will use that to create an object, then directly access that object’s data members to place the data into the object. Like classes, the compiler provides a public default constructor for structs if you do not define a custom constructor.

52      // deserialize JSON from text file into vector of Records
53      if (ifstream input{"records.json"}) {
54         cereal::JSONInputArchive archive{input};
55         vector<Record> deserializedRecords{};
56         archive(deserializedRecords); // deserialize records
57         cout << "
Deserialized records:
";
58         displayRecords(deserializedRecords);
59      }
60   }
Deserialized records:
100 Brian Blue 123.45
200 Sue Green 987.65

A cereal::JSONInputArchive deserializes data in JSON format from a specified stream, such as the standard input stream or a stream representing a file. Line 53 attempts to open the file records.json for reading. If successful, line 54 creates the object archive of type cereal::JSONInputArchive and initializes it with the input ifstream object so archive can read JSON data from the records.json file. Line 55 creates an empty vector of Records into which we’ll read the JSON data. Line 56 uses the archive object to deserialize the file’s data into the deserializedRecords object. Part of deserializing the vector is deserializing its elements. This again results in calls to serialize (lines 21–27), but because archive is a cereal::JSONInputArchive, each call to serialize reads one Record’s JSON data, creates a Record object then inserts the data into it.

9.22.2 Serializing a vector of Objects containing private Data

It is also possible to serialize objects containing private data. To do so, you must declare the serialize function as a friend of the class, so it can access the class’s private data. To demonstrate serializing private data, we created a copy of Fig. 9.36 and replaced the aggregate Record definition with the class Record definition (lines 14–37) in Fig. 9.38.

 1   // fig09_37.cpp
 2   // Serializing and deserializing objects containing private data.
 3   #include <iostream>
 4   #include <fstream>
 5   #include <string>
 6   #include <string_view>
 7   #include <vector>
 8   #include <fmt/format.h> // In C++20, this will be #include <format>
 9   #include <cereal/archives/json.hpp>
10   #include <cereal/types/vector.hpp>
11
12   using namespace std;
13
14   class Record {
15      // declare serialize as a friend for direct access to private data
16      template<typename Archive>
17      friend void serialize(Archive& archive, Record& record);
18
19   public:
20      // constructor
21      explicit Record(int account = 0, string_view first = "",
22         string_view last = "", double balance = 0.0)
23         : m_account{account}, m_first{first},
24           m_last{last}, m_balance{balance} {}
25
26      // get member functions
27      int getAccount() const {return m_account;}
28      const string& getFirst() const {return m_first;}
29      const string& getLast() const {return m_last;}
30      double getBalance() const {return m_balance;}
31
32   private:
33      int m_account{};
34      string m_first{};
35      string m_last{};
36      double m_balance{};
37   };
38
39   // function template serialize is responsible for serializing and
40   // deserializing Record objects to/from the specified Archive
41   template <typename Archive>
42   void serialize(Archive& archive, Record& record) {
43      archive(cereal::make_nvp("account", record.m_account),
44         cereal::make_nvp("first", record.m_first),
45         cereal::make_nvp("last", record.m_last),
46         cereal::make_nvp("balance", record.m_balance));
47   }
48
49   // display record at command line
50   void displayRecords(const vector<Record>& records) {
51      for (auto& r : records) {
52         cout << fmt::format("{} {} {} {:.2f}
", r.getAccount(),
53                    r.getFirst(), r.getLast(), r.getBalance());
54      }
55   }
56
57   int main() {
58      vector records{
59         Record{100, "Brian", "Blue", 123.45},
60         Record{200, "Sue", "Green", 987.65}
61      };
62
63      cout << "Records to serialize:
";
64      displayRecords(records);
65
66      // serialize vector of Records to JSON and store in text file
67      if (ofstream output{"records2.json"}) {
68         cereal::JSONOutputArchive archive{output};
69         archive(cereal::make_nvp("records", records)); // serialize records
70      }
71
72      // deserialize JSON from text file into vector of Records
73      if (ifstream input{"records2.json"}) {
74         cereal::JSONInputArchive archive{input};
75         vector<Record> deserializedRecords{};
76         archive(deserializedRecords); // deserialize records
77         cout << "
Deserialized records:
";
78         displayRecords(deserializedRecords);
79      }
80   }

Fig. 9.38 Serializing and deserializing objects containing private data.

This Record class provides a constructor (21–24), get member functions (lines 27–30) and private data (lines 33–36). There are two key items to note about this class:

• Lines 16–17 declare the function template serialize as a friend of this class. This enables serialize to access directly the data members account, first, last and balance.

• The constructor’s parameters all have default arguments, which allows cereal to use this as the default constructor when deserializing Record objects.

The serialize function (lines 41–46) now accesses class Record’s private data members, and the displayRecords function (lines 50–55) now uses each Record’s get functions to access the data to display. The main function is identical to section Section 9.22.1 and produces the same results, so we do not show the output here.

9.23 Wrap-Up

In this chapter, you created your own classes, created objects of those classes and called member functions of those objects to perform useful actions. You declared data members of a class to maintain data for each object of the class, and you defined member functions to operate on that data. You also learned how to use a class’s constructor to specify the initial values for an object’s data members.

We used a Time class case study to introduce various additional features. We showed how to engineer a class to separate its interface from its implementation. You used the arrow operator to access an object’s members via a pointer to an object. You saw that member functions have class scope—the member function’s name is known only to the class’s other member functions unless referred to by a client of the class via an object name, a reference to an object of the class, a pointer to an object of the class or the scope resolution operator. We also discussed access functions (commonly used to retrieve the values of data members or to test whether a condition is true or false), and utility functions (private member functions that support the operation of the class’s public member functions).

You saw that a constructor can specify default arguments that enable it to be called multiple ways. You also saw that any constructor that can be called with no arguments is a default constructor and that there can be at most one default constructor per class. We demonstrated how to share code among constructors with delegating constructors. We discussed destructors for performing termination housekeeping on an object before that object is destroyed, and demonstrated the order in which an object’s constructors and destructors are called.

We showed the problems that can occur when a member function returns a reference or a pointer to a private data member, which breaks the class’s encapsulation. We also showed that objects of the same type can be assigned to one another using the default assignment operator.

You learned how to specify const objects and const member functions to prevent modifications to objects, thus enforcing the principle of least privilege. You also learned that, through composition, a class can have objects of other classes as members. We demonstrated how to declare and use friend functions.

You saw that the this pointer is passed as an implicit argument to each of a class’s non-static member functions, allowing them to access the correct object’s data members and other non-static member functions. We used the this pointer explicitly to access the class’s members and to enable cascaded member-function calls. We motivated the notion of static data members and member functions and demonstrated how to declare and use them.

We introduced aggregate types and C++20’s designated initializers for aggregates. Finally, we presented our next “objects natural” case study on serializing objects with JSON (JavaScript Object Notation) using the cereal library.

In the next chapter, we continue our discussion of classes by introducing inheritance. We’ll see classes that share common attributes and behavior can inherit them from a common “base” class. Then, we build on our discussion of inheritance by introducing polymorphism. This object-oriented concept enables us to write programs that handle, in a more general manner, objects of classes related by inheritance.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset