Skip to content

7.3. Additional Class Features

The Sales_data class is pretty simple, yet it allowed us to explore quite a bit of the language support for classes. In this section, we’ll cover some additional class-related features that Sales_data doesn’t need to use. These features include type members, in-class initializers for members of class type, mutable data members, inline member functions, returning *this from a member function, more about how we define and use class types, and class friendship.

7.3.1. Class Members Revisited

To explore several of these additional features, we’ll define a pair of cooperating classes named Screen and Window_mgr.

Defining a Type Member

A Screen represents a window on a display. Each Screen has a string member that holds the Screen’s contents, and three string::size_type members that represent the position of the cursor, and the height and width of the screen.

In addition to defining data and function members, a class can define its own local names for types. Type names defined by a class are subject to the same access controls as any other member and may be either public or private:

c++
class Screen {
public:
    typedef std::string::size_type pos;
private:
    pos cursor = 0;
    pos height = 0, width = 0;
    std::string contents;
};

We defined pos in the public part of Screen because we want users to use that name. Users of Screen shouldn’t know that Screen uses a string to hold its data. By defining pos as a public member, we can hide this detail of how Screen is implemented.

There are two points to note about the declaration of pos. First, although we used a typedef2.5.1, p. 67), we can equivalently use a type alias (§ 2.5.1, p. 68):

c++
class Screen {
public:
    // alternative way to declare a type member using a type alias
    using pos = std::string::size_type;
    // other members as before
};

The second point is that, for reasons we’ll explain in § 7.4.1 (p. 284), unlike ordinary members, members that define types must appear before they are used. As a result, type members usually appear at the beginning of the class.

Member Functions of class Screen

To make our class more useful, we’ll add a constructor that will let users define the size and contents of the screen, along with members to move the cursor and to get the character at a given location:

c++
class Screen {
public:
    typedef std::string::size_type pos;
    Screen() = default; // needed because Screen has another constructor
    // cursor initialized to 0 by its in-class initializer
    Screen(pos ht, pos wd, char c): height(ht), width(wd),
                                    contents(ht * wd, c) { }
    char get() const              // get the character at the cursor
        { return contents[cursor]; }       // implicitly inline
    inline char get(pos ht, pos wd) const; // explicitly inline
    Screen &move(pos r, pos c);      // can be made inline later
private:
    pos cursor = 0;
    pos height = 0, width = 0;
    std::string contents;
};

Because we have provided a constructor, the compiler will not automatically generate a default constructor for us. If our class is to have a default constructor, we must say so explicitly. In this case, we use = default to ask the compiler to synthesize the default constructor’s definition for us (§ 7.1.4, p. 264).

It’s also worth noting that our second constructor (that takes three arguments) implicitly uses the in-class initializer for the cursor member (§ 7.1.4, p. 266). If our class did not have an in-class initializer for cursor, we would have explicitly initialized cursor along with the other members.

Making Members inline

Classes often have small functions that can benefit from being inlined. As we’ve seen, member functions defined inside the class are automatically inline6.5.2, p. 238). Thus, Screen’s constructors and the version of get that returns the character denoted by the cursor are inline by default.

We can explicitly declare a member function as inline as part of its declaration inside the class body. Alternatively, we can specify inline on the function definition that appears outside the class body:

c++
inline                   // we can specify inline on the definition
Screen &Screen::move(pos r, pos c)
{
    pos row = r * width; // compute the row location
    cursor = row + c ;   // move cursor to the column within that row
    return *this;        // return this object as an lvalue
}
char Screen::get(pos r, pos c) const // declared as inline in the class
{
    pos row = r * width;      // compute row location
    return contents[row + c]; // return character at the given column
}

Although we are not required to do so, it is legal to specify inline on both the declaration and the definition. However, specifying inline only on the definition outside the class can make the class easier to read.

INFO

For the same reasons that we define inline functions in headers (§ 6.5.2, p. 240), inline member functions should be defined in the same header as the corresponding class definition.

Overloading Member Functions

As with nonmember functions, member functions may be overloaded (§ 6.4, p. 230) so long as the functions differ by the number and/or types of parameters. The same function-matching (§ 6.4, p. 233) process is used for calls to member functions as for nonmember functions.

For example, our Screen class defined two versions of get. One version returns the character currently denoted by the cursor; the other returns the character at a given position specified by its row and column. The compiler uses the number of arguments to determine which version to run:

c++
Screen myscreen;
char ch = myscreen.get();// calls Screen::get()
ch = myscreen.get(0,0);  // calls Screen::get(pos, pos)
mutable Data Members

It sometimes (but not very often) happens that a class has a data member that we want to be able to modify, even inside a const member function. We indicate such members by including the mutable keyword in their declaration.

A mutabledata member is never const, even when it is a member of a const object. Accordingly, a const member function may change a mutable member. As an example, we’ll give Screen a mutable member named access_ctr, which we’ll use to track how often each Screen member function is called:

c++
class Screen {
public:
    void some_member() const;
private:
    mutable size_t access_ctr; // may change even in a const object
    // other members as before
};
void Screen::some_member() const
{
    ++access_ctr;    // keep a count of the calls to any member function
    // whatever other work this member needs to do
}

Despite the fact that some_member is a const member function, it can change the value of access_ctr. That member is a mutable member, so any member function, including const functions, can change its value.

Initializers for Data Members of Class Type
C++11

In addition to defining the Screen class, we’ll define a window manager class that represents a collection of Screens on a given display. This class will have a vector of Screens in which each element represents a particular Screen. By default, we’d like our Window_mgr class to start up with a single, default-initialized Screen. Under the new standard, the best way to specify this default value is as an in-class initializer (§ 2.6.1, p. 73):

c++
class Window_mgr {
private:
    // Screens this Window_mgr is tracking
    // by default, a Window_mgr has one standard sized blank Screen
    std::vector<Screen> screens{Screen(24, 80, ' ') };
};

When we initialize a member of class type, we are supplying arguments to a constructor of that member’s type. In this case, we list initialize our vector member (§ 3.3.1, p. 98) with a single element initializer. That initializer contains a Screen value that is passed to the vector<Screen> constructor to create a one-element vector. That value is created by the Screen constructor that takes two size parameters and a character to create a blank screen of the given size.

As we’ve seen, in-class initializers must use either the = form of initialization (which we used when we initialized the the data members of Screen) or the direct form of initialization using curly braces (as we do for screens).

INFO

When we provide an in-class initializer, we must do so following an = sign or inside braces.

INFO

Exercises Section 7.3.1

Exercise 7.23: Write your own version of the Screen class.

Exercise 7.24: Give your Screen class three constructors: a default constructor; a constructor that takes values for height and width and initializes the contents to hold the given number of blanks; and a constructor that takes values for height, width, and a character to use as the contents of the screen.

Exercise 7.25: Can Screen safely rely on the default versions of copy and assignment? If so, why? If not, why not?

Exercise 7.26: Define Sales_data::avg_price as an inline function.

7.3.2. Functions That Return *this

Fundamental

Next we’ll add functions to set the character at the cursor or at a given location:

c++
class Screen {
public:
    Screen &set(char);
    Screen &set(pos, pos, char);
    // other members as before
};
inline Screen &Screen::set(char c)
{
    contents[cursor] = c; // set the new value at the current cursor location
    return *this;         // return this object as an lvalue
}
inline Screen &Screen::set(pos r, pos col, char ch)
{
    contents[r*width + col] = ch;  // set specified location to given value
    return *this;                  // return this object as an lvalue
}

Like the move operation, our set members return a reference to the object on which they are called (§ 7.1.2, p. 259). Functions that return a reference are lvalues (§ 6.3.2, p. 226), which means that they return the object itself, not a copy of the object. If we concatenate a sequence of these actions into a single expression:

c++
// move the cursor to a given position, and set that character
myScreen.move(4,0).set('#');

these operations will execute on the same object. In this expression, we first move the cursor inside myScreen and then set a character in myScreen’s contents member. That is, this statement is equivalent to

c++
myScreen.move(4,0);
myScreen.set('#');

Had we defined move and set to return Screen, rather than Screen&, this statement would execute quite differently. In this case it would be equivalent to:

c++
// if move returns Screen not Screen&
Screen temp = myScreen.move(4,0);  // the return value would be copied
temp.set('#'); // the contents inside myScreen would be unchanged

If move had a nonreference return type, then the return value of move would be a copy of *this6.3.2, p. 224). The call to set would change the temporary copy, not myScreen.

Returning *this from a const Member Function

Next, we’ll add an operation, which we’ll name display, to print the contents of the Screen. We’d like to be able to include this operation in a sequence of set and move operations. Therefore, like set and move, our display function will return a reference to the object on which it executes.

Logically, displaying a Screen doesn’t change the object, so we should make display a const member. If display is a const member, then this is a pointer to const and *this is a const object. Hence, the return type of display must be const Sales_data&. However, if display returns a reference to const, we won’t be able to embed display into a series of actions:

c++
Screen myScreen;
// if display returns a const reference, the call to set is an error
myScreen.display(cout).set('*');

Even though myScreen is a nonconst object, the call to set won’t compile. The problem is that the const version of display returns a reference to const and we cannot call set on a const object.

INFO

A const member function that returns *this as a reference should have a return type that is a reference to const.

Overloading Based on const

We can overload a member function based on whether it is const for the same reasons that we can overload a function based on whether a pointer parameter points to const6.4, p. 232). The nonconst version will not be viable for const objects; we can only call const member functions on a const object. We can call either version on a nonconst object, but the nonconst version will be a better match.

In this example, we’ll define a private member named do_display to do the actual work of printing the Screen. Each of the display operations will call this function and then return the object on which it is executing:

c++
class Screen {
public:
    // display overloaded on whether the object is const or not
    Screen &display(std::ostream &os)
                  { do_display(os); return *this; }
    const Screen &display(std::ostream &os) const
                  { do_display(os); return *this; }
private:
     // function to do the work of displaying a Screen
     void do_display(std::ostream &os) const {os << contents;}
    // other members as before
};

As in any other context, when one member calls another the this pointer is passed implicitly. Thus, when display calls do_display, its own this pointer is implicitly passed to do_display. When the nonconst version of display calls do_display, its this pointer is implicitly converted from a pointer to nonconst to a pointer to const4.11.2, p. 162).

When do_display completes, the display functions each return the object on which they execute by dereferencing this. In the nonconst version, this points to a nonconst object, so that version of display returns an ordinary (nonconst) reference; the const member returns a reference to const.

When we call display on an object, whether that object is const determines which version of display is called:

c++
Screen myScreen(5,3);
const Screen blank(5, 3);
myScreen.set('#').display(cout);   // calls non const version
blank.display(cout);               // calls const version

INFO

Advice: Use Private Utility Functions for Common Code

Some readers might be surprised that we bothered to define a separate do_display operation. After all, the calls to do_display aren’t much simpler than the action done inside do_display. Why bother? We do so for several reasons:

  • A general desire to avoid writing the same code in more than one place.
  • We expect that the display operation will become more complicated as our class evolves. As the actions involved become more complicated, it makes more obvious sense to write those actions in one place, not two.
  • It is likely that we might want to add debugging information to do_display during development that would be eliminated in the final product version of the code. It will be easier to do so if only one definition of do_display needs to be changed to add or remove the debugging code.
  • There needn’t be any overhead involved in this extra function call. We defined do_display inside the class body, so it is implicitly inline. Thus, there likely be no run-time overhead associating with calling do_display.

In practice, well-designed C++ programs tend to have lots of small functions such as do_display that are called to do the “real” work of some other set of functions.

7.3.3. Class Types

Every class defines a unique type. Two different classes define two different types even if they define the same members. For example:

INFO

Exercises Section 7.3.2

Exercise 7.27: Add the move, set, and display operations to your version of Screen. Test your class by executing the following code:

c++
Screen myScreen(5, 5, 'X');
myScreen.move(4,0).set('#').display(cout);
cout << "\n";
myScreen.display(cout);
cout << "\n";

Exercise 7.28: What would happen in the previous exercise if the return type of move, set, and display was Screen rather than Screen&?

Exercise 7.29: Revise your Screen class so that move, set, and display functions return Screen and check your prediction from the previous exercise.

Exercise 7.30: It is legal but redundant to refer to members through the this pointer. Discuss the pros and cons of explicitly using the this pointer to access members.

c++
struct First {
    int memi;
    int getMem();
};
struct Second {
    int memi;
    int getMem();
};
First obj1;
Second obj2 = obj1; // error: obj1 and obj2 have different types

INFO

Even if two classes have exactly the same member list, they are different types. The members of each class are distinct from the members of any other class (or any other scope).

We can refer to a class type directly, by using the class name as a type name. Alternatively, we can use the class name following the keyword class or struct:

c++
Sales_data item1;       // default-initialized object of type Sales_data
class Sales_data item1; // equivalent declaration

Both methods of referring to a class type are equivalent. The second method is inherited from C and is also valid in C++.

Class Declarations

Just as we can declare a function apart from its definition (§ 6.1.2, p. 206), we can also declare a class without defining it:

c++
class Screen; // declaration of the Screen class

This declaration, sometimes referred to as a forward declaration, introduces the name Screen into the program and indicates that Screen refers to a class type. After a declaration and before a definition is seen, the type Screen is an incomplete type—it’s known that Screen is a class type but not known what members that type contains.

We can use an incomplete type in only limited ways: We can define pointers or references to such types, and we can declare (but not define) functions that use an incomplete type as a parameter or return type.

A class must be defined—not just declared—before we can write code that creates objects of that type. Otherwise, the compiler does not know how much storage such objects need. Similarly, the class must be defined before a reference or pointer is used to access a member of the type. After all, if the class has not been defined, the compiler can’t know what members the class has.

With one exception that we’ll describe in § 7.6 (p. 300), data members can be specified to be of a class type only if the class has been defined. The type must be complete because the compiler needs to know how much storage the data member requires. Because a class is not defined until its class body is complete, a class cannot have data members of its own type. However, a class is considered declared (but not yet defined) as soon as its class name has been seen. Therefore, a class can have data members that are pointers or references to its own type:

c++
class Link_screen {
    Screen window;
    Link_screen *next;
    Link_screen *prev;
};

INFO

Exercises Section 7.3.3

Exercise 7.31: Define a pair of classes X and Y, in which X has a pointer to Y, and Y has an object of type X.

7.3.4. Friendship Revisited

Our Sales_data class defined three ordinary nonmember functions as friends (§ 7.2.1, p. 269). A class can also make another class its friend or it can declare specific member functions of another (previously defined) class as friends. In addition, a friend function can be defined inside the class body. Such functions are implicitly inline.

Friendship between Classes

As an example of class friendship, our Window_mgr class (§ 7.3.1, p. 274) will have members that will need access to the internal data of the Screen objects it manages. For example, let’s assume that we want to add a member, named clearto Window_mgr that will reset the contents of a particular Screen to all blanks. To do this job, clear needs to access the private data members of Screen. To allow this access, Screen can designate Window_mgr as its friend:

c++
class Screen {
    // Window_mgr members can access the private parts of class Screen
    friend class Window_mgr;
    // ... rest of the Screen class
};

The member functions of a friend class can access all the members, including the nonpublic members, of the class granting friendship. Now that Window_mgr is a friend of Screen, we can write the clear member of Window_mgr as follows:

c++
class Window_mgr {
public:
    // location ID for each screen on the window
    using ScreenIndex = std::vector<Screen>::size_type;
    // reset the Screen at the given position to all blanks
    void clear(ScreenIndex);
private:
    std::vector<Screen> screens{Screen(24, 80, ' ')};
};
void Window_mgr::clear(ScreenIndex i)
{
    // s is a reference to the Screen we want to clear
    Screen &s = screens[i];
    // reset the contents of that Screen to all blanks
    s.contents = string(s.height * s.width, ' ');
}

We start by defining s as a reference to the Screen at position i in the screens vector. We then use the height and width members of that Screen to compute anew string that has the appropriate number of blank characters. We assign that string of blanks to the contents member.

If clear were not a friend of Screen, this code would not compile. The clear function would not be allowed to use the height width, or contents members of Screen. Because Screen grants friendship to Window_mgr, all the members of Screen are accessible to the functions in Window_mgr.

It is important to understand that friendship is not transitive. That is, if class Window_mgr has its own friends, those friends have no special access to Screen.

INFO

Each class controls which classes or functions are its friends.

Making A Member Function a Friend

Rather than making the entire Window_mgr class a friend, Screen can instead specify that only the clear member is allowed access. When we declare a member function to be a friend, we must specify the class of which that function is a member:

c++
class Screen {
    // Window_mgr::clear must have been declared before class Screen
    friend void Window_mgr::clear(ScreenIndex);
    // ... rest of the Screen class
};

Making a member function a friend requires careful structuring of our programs to accommodate interdependencies among the declarations and definitions. In this example, we must order our program as follows:

  • First, define the Window_mgr class, which declares, but cannot define, clear. Screen must be declared before clear can use the members of Screen.
  • Next, define class Screen, including a friend declaration for clear.
  • Finally, define clear, which can now refer to the members in Screen.
Overloaded Functions and Friendship

Although overloaded functions share a common name, they are still different functions. Therefore, a class must declare as a friend each function in a set of overloaded functions that it wishes to make a friend:

c++
// overloaded storeOn functions
extern std::ostream& storeOn(std::ostream &, Screen &);
extern BitMap& storeOn(BitMap &, Screen &);
class Screen {
    // ostream version of storeOn may access the private parts of Screen objects
    friend std::ostream& storeOn(std::ostream &, Screen &);
    // . . .
};

Class Screen makes the version of storeOn that takes an ostream& its friend. The version that takes a BitMap& has no special access to Screen.

Friend Declarations and Scope

Classes and nonmember functions need not have been declared before they are used in a friend declaration. When a name first appears in a friend declaration, that name is implicitly assumed to be part of the surrounding scope. However, the friend itself is not actually declared in that scope (§ 7.2.1, p. 270).

Even if we define the function inside the class, we must still provide a declaration outside of the class itself to make that function visible. A declaration must exist even if we only call the friend from members of the friendship granting class:

c++
struct X {
    friend void f() { /* friend function can be defined in the class body   */ }
    X() { f(); } // error: no declaration for f
    void g();
    void h();
};
void X::g() { return f(); } // error: f hasn't been declared
void f();                   // declares the function defined inside X
void X::h() { return f(); } // ok: declaration for f is now in scope

It is important to understand that a friend declaration affects access but is not a declaration in an ordinary sense.

INFO

Remember, some compilers do not enforce the lookup rules for friends (§ 7.2.1, p. 270).

INFO

Exercises Section 7.3.4

Exercise 7.32: Define your own versions of Screen and Window_mgr in which clear is a member of Window_mgr and a friend of Screen.