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
).
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.