15.6. Class Scope under Inheritance
FundamentalEach class defines its own scope (§7.4, p. 282) within which its members are defined. Under inheritance, the scope of a derived class is nested (§2.2.4, p. 48) inside the scope of its base classes. If a name is unresolved within the scope of the derived class, the enclosing base-class scopes are searched for a definition of that name.
The fact that the scope of a derived class nests inside the scope of its base classes can be surprising. After all, the base and derived classes are defined in separate parts of our program’s text. However, it is this hierarchical nesting of class scopes that allows the members of a derived class to use members of its base class as if those members were part of the derived class. For example, when we write
Bulk_quote bulk;
cout << bulk.isbn();
the use of the name isbn
is resolved as follows:
- Because we called
isbn
on an object of typeBulk_quote
, the search starts in theBulk_quote
class. The nameisbn
is not found in that class. - Because
Bulk_quote
is derived fromDisc_quote
, theDisc_quote
class is searched next. The name is still not found. - Because
Disc_quote
is derived fromQuote
, theQuote
class is searched next. The nameisbn
is found in that class; the use ofisbn
is resolved to theisbn
inQuote
.
Name Lookup Happens at Compile Time
The static type (§15.2.3, p. 601) of an object, reference, or pointer determines which members of that object are visible. Even when the static and dynamic types might differ (as can happen when a reference or pointer to a base class is used), the static type determines what members can be used. As an example, we might add a member to the Disc_quote
class that returns a pair
(§11.2.3, p. 426) holding the minimum (or maximum) quantity and the discounted price:
class Disc_quote : public Quote {
public:
std::pair<size_t, double> discount_policy() const
{ return {quantity, discount}; }
// other members as before
};
We can use discount_policy
only through an object, pointer, or reference of type Disc_quote
or of a class derived from Disc_quote
:
Bulk_quote bulk;
Bulk_quote *bulkP = &bulk; // static and dynamic types are the same
Quote *itemP = &bulk; // static and dynamic types differ
bulkP->discount_policy(); // ok: bulkP has type Bulk_quote*
itemP->discount_policy(); // error: itemP has type Quote*
Even though bulk
has a member named discount_policy
, that member is not visible through itemP
. The type of itemP
is a pointer to Quote
, which means that the search for discount_policy
starts in class Quote
. The Quote
class has no member named discount_policy
, so we cannot call that member on an object, reference, or pointer of type Quote
.
Name Collisions and Inheritance
Like any other scope, a derived class can reuse a name defined in one of its direct or indirect base classes. As usual, names defined in an inner scope (e.g., a derived class) hide uses of that name in the outer scope (e.g., a base class) (§2.2.4, p. 48):
struct Base {
Base(): mem(0) { }
protected:
int mem;
};
struct Derived : Base {
Derived(int i): mem(i) { } // initializes Derived::mem to i
// Base::mem is default initialized
int get_mem() { return mem; } // returns Derived::mem
protected:
int mem; // hides mem in the base
};
The reference to mem
inside get_mem
is resolved to the name inside Derived
. Were we to write
Derived d(42);
cout << d.get_mem() << endl; // prints 42
then the output would be 42
.
INFO
A derived-class member with the same name as a member of the base class hides direct use of the base-class member.
Using the Scope Operator to Use Hidden Members
We can use a hidden base-class member by using the scope operator:
struct Derived : Base {
int get_base_mem() { return Base::mem; }
// ...
};
The scope operator overrides the normal lookup and directs the compiler to look for mem
starting in the scope of class Base
. If we ran the code above with this version of Derived
, the result of d.get_mem()
would be 0
.
TIP
Best Practices
Aside from overriding inherited virtual functions, a derived class usually should not reuse names defined in its base class.
INFO
Key Concept: Name Lookup and Inheritance
Understanding how function calls are resolved is crucial to understanding inheritance in C++. Given the call p->mem()
(or obj.mem()
), the following four steps happen:
- First determine the static type of
p
(orobj
). Because we’re calling a member, that type must be a class type. - Look for
mem
in the class that corresponds to the static type ofp
(orobj
). Ifmem
is not found, look in the direct base class and continue up the chain of classes untilmem
is found or the last class is searched. Ifmem
is not found in the class or its enclosing base classes, then the call will not compile. - Once
mem
is found, do normal type checking (§6.1, p. 203) to see if this call is legal given the definition that was found. - Assuming the call is legal, the compiler generates code, which varies depending on whether the call is virtual or not:
- If
mem
is virtual and the call is made through a reference or pointer, then the compiler generates code to determine at run time which version to run based on the dynamic type of the object. - Otherwise, if the function is nonvirtual, or if the call is on an object (not a reference or pointer), the compiler generates a normal function call.
As Usual, Name Lookup Happens before Type Checking
As we’ve seen, functions declared in an inner scope do not overload functions declared in an outer scope (§6.4.1, p. 234). As a result, functions defined in a derived class do not overload members defined in its base class(es). As in any other scope, if a member in a derived class (i.e., in an inner scope) has the same name as a base-class member (i.e., a name defined in an outer scope), then the derived member hides the base-class member within the scope of the derived class. The base member is hidden even if the functions have different parameter lists:
struct Base {
int memfcn();
};
struct Derived : Base {
int memfcn(int); // hides memfcn in the base
};
Derived d; Base b;
b.memfcn(); // calls Base::memfcn
d.memfcn(10); // calls Derived::memfcn
d.memfcn(); // error: memfcn with no arguments is hidden
d.Base::memfcn(); // ok: calls Base::memfcn
The declaration of memfcn
in Derived
hides the declaration of memfcn
in Base
. Not surprisingly, the first call through b
, which is a Base
object, calls the version in the base class. Similarly, the second call (through d
) calls the one from Derived
. What can be surprising is that the third call, d.memfcn()
, is illegal.
To resolve this call, the compiler looks for the name memfcn
in Derived
. That class defines a member named memfcn
and the search stops. Once the name is found, the compiler looks no further. The version of memfcn
in Derived
expects an int
argument. This call provides no such argument; it is in error.
Virtual Functions and Scope
TrickyWe can now understand why virtual functions must have the same parameter list in the base and derived classes (§15.3, p. 605). If the base and derived members took arguments that differed from one another, there would be no way to call the derived version through a reference or pointer to the base class. For example:
class Base {
public:
virtual int fcn();
};
class D1 : public Base {
public:
// hides fcn in the base; this fcn is not virtual
// D1 inherits the definition of Base::fcn()
int fcn(int); // parameter list differs from fcn in Base
virtual void f2(); // new virtual function that does not exist in Base
};
class D2 : public D1 {
public:
int fcn(int); // nonvirtual function hides D1::fcn(int)
int fcn(); // overrides virtual fcn from Base
void f2(); // overrides virtual f2 from D1
};
The fcn
function in D1
does not override the virtual fcn
from Base
because they have different parameter lists. Instead, it hidesfcn
from the base. Effectively, D1
has two functions named fcn
: D1
inherits a virtual named fcn
from Base
and defines its own, nonvirtual member named fcn
that takes an int
parameter.
Calling a Hidden Virtual through the Base Class
Given the classes above, let’s look at several different ways to call these functions:
Base bobj; D1 d1obj; D2 d2obj;
Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
bp1->fcn(); // virtual call, will call Base::fcn at run time
bp2->fcn(); // virtual call, will call Base::fcn at run time
bp3->fcn(); // virtual call, will call D2::fcn at run time
D1 *d1p = &d1obj; D2 *d2p = &d2obj;
bp2->f2(); // error: Base has no member named f2
d1p->f2(); // virtual call, will call D1::f2() at run time
d2p->f2(); // virtual call, will call D2::f2() at run time
The first three calls are all made through pointers to the base class. Because fcn
is virtual, the compiler generates code to decide at run time which version to call. That decision will be based on the actual type of the object to which the pointer is bound. In the case of bp2
, the underlying object is a D1
. That class did not override the fcn
function that takes no arguments. Thus, the call through bp2
is resolved (at run time) to the version defined in Base
.
The next three calls are made through pointers with differing types. Each pointer points to one of the types in this hierarchy. The first call is illegal because there is no f2()
in class Base
. The fact that the pointer happens to point to a derived object is irrelevant.
For completeness, let’s look at calls to the nonvirtual function fcn(int)
:
Base *p1 = &d2obj; D1 *p2 = &d2obj; D2 *p3 = &d2obj;
p1->fcn(42); // error: Base has no version of fcn that takes an int
p2->fcn(42); // statically bound, calls D1::fcn(int)
p3->fcn(42); // statically bound, calls D2::fcn(int)
In each call the pointer happens to point to an object of type D2
. However, the dynamic type doesn’t matter when we call a nonvirtual function. The version that is called depends only on the static type of the pointer.
Overriding Overloaded Functions
As with any other function, a member function (virtual or otherwise) can be overloaded. A derived class can override zero or more instances of the overloaded functions it inherits. If a derived class wants to make all the overloaded versions available through its type, then it must override all of them or none of them.
Sometimes a class needs to override some, but not all, of the functions in an overloaded set. It would be tedious in such cases to have to override every base-class version in order to override the ones that the class needs to specialize.
Instead of overriding every base-class version that it inherits, a derived class can provide a using
declaration (§15.5, p. 615) for the overloaded member. A using
declaration specifies only a name; it may not specify a parameter list. Thus, a using
declaration for a base-class member function adds all the overloaded instances of that function to the scope of the derived class. Having brought all the names into its scope, the derived class needs to define only those functions that truly depend on its type. It can use the inherited definitions for the others.
The normal rules for a using
declaration inside a class apply to names of overloaded functions (§15.5, p. 615); every overloaded instance of the function in the base class must be accessible to the derived class. The access to the overloaded versions that are not otherwise redefined by the derived class will be the access in effect at the point of the using
declaration.
INFO
Exercises Section 15.6
Exercise 15.23: Assuming class D1
on page 620 had intended to override its inherited fcn
function, how would you fix that class? Assuming you fixed the class so that fcn
matched the definition in Base
, how would the calls in that section be resolved?