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