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.
To explore several of these additional features, we’ll define a pair of cooperating classes named Screen
and Window_mgr
.
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
:
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 typedef
(§ 2.5.1, p. 67), we can equivalently use a type alias (§ 2.5.1, p. 68):
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.
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:
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.
inline
Classes often have small functions that can benefit from being inlined. As we’ve seen, member functions defined inside the class are automatically inline
(§ 6.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:
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.
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.
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:
Screen myscreen;
char ch = myscreen.get();// calls Screen::get()
ch = myscreen.get(0,0); // calls Screen::get(pos, pos)
mutable
Data MembersIt 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 mutable
data 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:
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.
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 Screen
s 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):
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
).
Exercises Section 7.3.1
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 aninline
function.
*this
Next we’ll add functions to set the character at the cursor or at a given location:
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:
// 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
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:
// 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 *this
(§ 6.3.2, p. 224). The call to set
would change the temporary copy, not myScreen
.
*this
from a const
Member FunctionNext, 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:
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.
A
const
member function that returns*this
as a reference should have a return type that is a reference toconst
.
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 const
(§ 6.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:
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 const
(§ 4.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:
Screen myScreen(5,3);
const Screen blank(5, 3);
myScreen.set('#').display(cout); // calls non const version
blank.display(cout); // calls const version
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 todo_display
aren’t much simpler than the action done insidedo_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 ofdo_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 implicitlyinline
. Thus, there likely be no run-time overhead associating with callingdo_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.
Every class defines a unique type. Two different classes define two different types even if they define the same members. For example:
Exercises Section 7.3.2
Exercise 7.27: Add the
move
,set
, anddisplay
operations to your version ofScreen
. Test your class by executing the following code: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
, anddisplay
wasScreen
rather thanScreen&?
Exercise 7.29: Revise your
Screen
class so thatmove
,set
, anddisplay
functions returnScreen
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 thethis
pointer to access members.
struct First {
int memi;
int getMem();
};
struct Second {
int memi;
int getMem();
};
First obj1;
Second obj2 = obj1; // error: obj1 and obj2 have different types
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
:
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++.
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:
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:
class Link_screen {
Screen window;
Link_screen *next;
Link_screen *prev;
};
Exercises Section 7.3.3
Exercise 7.31: Define a pair of classes
X
andY
, in whichX
has a pointer toY
, andY
has an object of typeX
.
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
.
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 clear
to 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:
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:
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
.
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:
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 beforeclear
can use the members ofScreen
.
• Next, define class
Screen
, including a friend declaration forclear
.
• Finally, define
clear
, which can now refer to the members inScreen
.
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:
// 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
.
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:
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.
Exercises Section 7.3.4
Exercise 7.32: Define your own versions of
Screen
andWindow_mgr
in whichclear
is a member ofWindow_mgr
and a friend ofScreen
.