Every class defines its own new scope. Outside the class scope, ordinary data and function members may be accessed only through an object, a reference, or a pointer using a member access operator (§ 4.6, p. 150). We access type members from the class using the scope operator . In either case, the name that follows the operator must be a member of the associated class.
Screen::pos ht = 24, wd = 80; // use the pos type defined by Screen
Screen scr(ht, wd, ' ');
Screen *p = &scr;
char c = scr.get(); // fetches the get member from the object scr
c = p->get(); // fetches the get member from the object to which p points
The fact that a class is a scope explains why we must provide the class name as well as the function name when we define a member function outside its class (§ 7.1.2, p. 259). Outside of the class, the names of the members are hidden.
Once the class name is seen, the remainder of the definition—including the parameter list and the function body—is in the scope of the class. As a result, we can refer to other class members without qualification.
For example, recall the
clear
member of class
Window_mgr
(§
7.3.4, p.
280).
That function’s parameter uses a type that is defined by
Window_mgr
:
void Window_mgr::clear(ScreenIndex i)
{
Screen &s = screens[i];
s.contents = string(s.height * s.width, ' ');
}
Because the compiler sees the parameter list after noting that we are in
the scope of class
Window_mgr
, there is no need to specify that we want the
ScreenIndex
that is defined by
Window_mgr
. For the same reason, the use of
screens
in the function body refers to name declared inside class
Window_mgr
.
On the other hand, the return type of a function normally appears before
the function’s name. When a member function is defined outside the
class body, any name used in the return type is outside the class scope.
As a result, the return type must specify the class of which it is a
member. For example, we might give
Window_mgr
a function, named
addScreen
, to add another screen to the display. This member will return a
ScreenIndex
value that the user can subsequently use to locate this
Screen
:
class Window_mgr {
public:
// add a Screen to the window and returns its index
ScreenIndex addScreen(const Screen&);
// other members as before
};
// return type is seen before we're in the scope of Window_mgr
Window_mgr::ScreenIndex
Window_mgr::addScreen(const Screen &s)
{
screens.push_back(s);
return screens.size() - 1;
}
Because the return type appears before the name of the class is seen, it
appears outside the scope of class
Window_mgr
. To use
ScreenIndex
for the return type, we must specify the class in which that type is
defined.
Exercises Section 7.4
Exercise 7.33: What would happen if we gave
Screen
asize
member defined as follows? Fix any problems you identify.
pos Screen::size() const
{
return height * width;
}
In the programs we’ve written so far, name lookup (the process of finding which declarations match the use of a name) has been relatively straightforward:
• First, look for a declaration of the name in the block in which the name was used. Only names declared before the use are considered.
• If the name isn’t found, look in the enclosing scope(s).
• If no declaration is found, then the program is in error.
The way names are resolved inside member functions defined inside the class may seem to behave differently than these lookup rules. However, in this case, appearances are deceiving. Class definitions are processed in two phases:
• First, the member declarations are compiled.
• Function bodies are compiled only after the entire class has been seen.
Member function definitions are processed after the compiler processes all of the declarations in the class.
Classes are processed in this two-phase way to make it easier to organize class code. Because member function bodies are not processed until the entire class is seen, they can use any name defined inside the class. If function definitions were processed at the same time as the member declarations, then we would have to order the member functions so that they referred only to names already seen.
This two-step process applies only to names used in the body of a member function. Names used in declarations, including names used for the return type and types in the parameter list, must be seen before they are used. If a member declaration uses a name that has not yet been seen inside the class, the compiler will look for that name in the scope(s) in which the class is defined. For example:
typedef double Money;
string bal;
class Account {
public:
Money balance() { return bal; }
private:
Money bal;
// ...
};
When the compiler sees the declaration of the
balance
function, it will look for a declaration of
Money
in the
Account
class. The compiler considers only declarations inside
Account
that appear before the use of
Money
. Because no matching member is found, the compiler then looks for a
declaration in the enclosing scope(s). In this example, the compiler will
find the
typedef
of
Money
. That type will be used for the return type of the function
balance
and as the type for the data member
bal
. On the other hand, the function body of
balance
is processed only after the entire class is seen. Thus, the
return
inside that function returns the member named
bal
, not the
string
from the outer scope.
Ordinarily, an inner scope can redefine a name from an outer scope even if that name has already been used in the inner scope. However, in a class, if a member uses a name from an outer scope and that name is a type, then the class may not subsequently redefine that name:
typedef double Money;
class Account {
public:
Money balance() { return bal; } // uses Money from the outer scope
private:
typedef double Money; // error: cannot redefine Money
Money bal;
// ...
};
It is worth noting that even though the definition of
Money
inside
Account
uses the same type as the definition in the outer scope, this code is
still in error.
Although it is an error to redefine a type name, compilers are not required to diagnose this error. Some compilers will quietly accept such code, even though the program is in error.
Definitions of type names usually should appear at the beginning of a class. That way any member that uses that type will be seen after the type name has already been defined.
A name used in the body of a member function is resolved as follows:
• First, look for a declaration of the name inside the member function. As usual, only declarations in the function body that precede the use of the name are considered.
• If the declaration is not found inside the member function, look for a declaration inside the class. All the members of the class are considered.
• If a declaration for the name is not found in the class, look for a declaration that is in scope before the member function definition.
Ordinarily, it is a bad idea to use the name of another member as the name
for a parameter in a member function. However, in order to show how names
are resolved, we’ll violate that normal practice in our
dummy_fcn
function:
// note: this code is for illustration purposes only and reflects bad practice
// it is generally a bad idea to use the same name for a parameter and a member
int height; // defines a name subsequently used inside Screen
class Screen {
public:
typedef std::string::size_type pos;
void dummy_fcn(pos height) {
cursor = width * height; // which height? the parameter
}
private:
pos cursor = 0;
pos height = 0, width = 0;
};
When the compiler processes the multiplication expression inside
dummy_fcn
, it first looks for the names used in that expression in the scope of
that function. A function’s parameters are in the function’s
scope. Thus, the name
height
, used in the body of
dummy_fcn
, refers to this parameter declaration.
In this case, the
height
parameter hides the member named
height
. If we wanted to override the normal lookup rules, we can do so:
// bad practice: names local to member functions shouldn't hide member names
void Screen::dummy_fcn(pos height) {
cursor = width * this->height; // member height
// alternative way to indicate the member
cursor = width * Screen::height; // member height
}
Even though the class member is hidden, it is still possible to use that member by qualifying the member’s name with the name of its class or by using the
this
pointer explicitly.
A much better way to ensure that we get the member named
height
would be to give the parameter a different name:
// good practice: don't use a member name for a parameter or other local variable
void Screen::dummy_fcn(pos ht) {
cursor = width * height; // member height
}
In this case, when the compiler looks for the name
height
, it won’t be found inside
dummy_fcn
. The compiler next looks at all the declarations in
Screen
. Even though the declaration of
height
appears after its use inside
dummy_fcn
, the compiler resolves this use to the data member named
height
.
If the compiler doesn’t find the name in function or class scope, it
looks for the name in the surrounding scope. In our example, the name
height
is defined in the outer scope before the definition of
Screen
. However, the object in the outer scope is hidden by our member named
height
. If we want the name from the outer scope, we can ask for it explicitly
using the scope operator:
// bad practice: don't hide names that are needed from surrounding scopes
void Screen::dummy_fcn(pos height) {
cursor = width * ::height;// which height? the global one
}
Even though the outer object is hidden, it is still possible to access that object by using the scope operator.
When a member is defined outside its class, the third step of name lookup includes names declared in the scope of the member definition as well as those that appear in the scope of the class definition. For example:
int height; // defines a name subsequently used inside Screen
class Screen {
public:
typedef std::string::size_type pos;
void setHeight(pos);
pos height = 0; // hides the declaration of height in the outer scope
};
Screen::pos verify(Screen::pos);
void Screen::setHeight(pos var) {
// var: refers to the parameter
// height: refers to the class member
// verify: refers to the global function
height = verify(var);
}
Notice that the declaration of the global function
verify
is not visible before the definition of the class
Screen
. However, the third step of name lookup includes the scope in which the
member definition appears. In this example, the declaration for
verify
appears before
setHeight
is defined and may, therefore, be used.
Exercises Section 7.4.1
Exercise 7.34: What would happen if we put the
typedef
ofpos
in theScreen
class on page 285 as the last line in the class?Exercise 7.35: Explain the following code, indicating which definition of
Type
orinitVal
is used for each use of those names. Say how you would fix any errors.
typedef string Type;
Type initVal();
class Exercise {
public:
typedef double Type;
Type setVal(Type);
Type initVal();
private:
int val;
};
Type Exercise::setVal(Type parm) {
val = parm + initVal();
return val;
}