7.4. Class Scope
FundamentalEvery 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
Scope and Members Defined outside the Class
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 WindowMgr
, there is no need to specify that we want the ScreenIndex
that is defined by WindowMgr
. 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.
INFO
Exercises Section 7.4
Exercise 7.33: What would happen if we gave Screen
a size
member defined as follows? Fix any problems you identify.
pos Screen::size() const
{
return height * width;
}
7.4.1. Name Lookup and Class Scope
TrickyIn 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.
INFO
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.
Name Lookup for Class Member Declarations
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.
Type Names Are Special
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.
TIP
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.
Normal Block-Scope Name Lookup inside Member Definitions
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
}
INFO
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
.
After Class Scope, Look in the Surrounding Scope
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
}
INFO
Even though the outer object is hidden, it is still possible to access that object by using the scope operator.
Names Are Resolved Where They Appear within a File
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.
INFO
Exercises Section 7.4.1
Exercise 7.34: What would happen if we put the typedef
of pos
in the Screen
class on page 285 as the last line in the class?
Exercise 7.35: Explain the following code, indicating which definition of Type
or initVal
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;
}