15.5. Class Scope under InheritanceEach class maintains its own scope (Section 12.3, p. 444) within which the names of its members are defined. Under inheritance, the scope of the derived class is nested within the scope of its base classes. If a name is unresolved within the scope of the derived class, the enclosing base-class scope(s) are searched for a definition of that name. It is this hierarchical nesting of class scopes under inheritance that allows the members of the base class to be accessed directly as if they are members of the derived class. When we write Bulk_item bulk; cout << bulk.book(); the use of the name book is resolved as follows:
15.5.1. Name Lookup Happens at Compile TimeThe static type of an object, reference, or pointer determines the actions that the object can perform. Even when the static and dynamic types might differ, as can happen when a reference or pointer to a base type is used, the static type determines what members can be used. As an example, we might add a member to the Disc_item class that returns a pair holding the minimum (or maximum) quantity and the discounted price:
class Disc_item : public Item_base {
public:
std::pair<size_t, double> discount_policy() const
{ return std::make_pair(quantity, discount); }
// other members as before
};
We can access discount_policy only through an object, pointer, or reference of type Disc_item or a class derived from Disc_item: Bulk_item bulk; Bulk_item *bulkP = &bulk; // ok: static and dynamic types are the same Item_base *itemP = &bulk; // ok: static and dynamic types differ bulkP->discount_policy(); // ok: bulkP has type Bulk_item* itemP->discount_policy(); // error: itemP has type Item_base* The call through itemP is an error because a pointer (reference or object) to a base type can access only the base parts of an object and there is no discount_policy member defined in the base class.
15.5.2. Name Collisions and InheritanceAlthough a base-class member can be accessed directly as if it were a member of the derived class, the member retains its base-class membership. Normally we do not care which actual class contains the member. We usually need to care only when a base- and derived-class member share the same name.
struct Base { Base(): mem(0) { } protected: int mem; }; struct Derived : Base { Derived(int i): mem(i) { } // initializes Derived::mem 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. Using the Scope Operator to Access Hidden MembersWe can access a hidden base-class member by using the scope operator: struct Derived : Base { int get_base_mem() { return Base::mem; } }; The scope operator directs the compiler to look for mem starting in Base.
15.5.3. Scope and Member FunctionsA member function with the same name in the base and derived class behaves the same way as a data member: The derived-class member hides the base-class member within the scope of the derived class. The base member is hidden, even if the prototypes of the functions differ: 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 in Base. Not surprisingly, the first call through b, which is aBase object, calls the version in the base class. Similarly, the second call through d calls the one from Derived. What can be surprising is the third call: d.memfcn(); // error: Derived has no memfcn that takes no arguments To resolve this call, the compiler looks for the name memfcn, which it finds in the class Derived. Once the name is found, the compiler looks no further. This call does not match the definition of memfcn in Derived, which expects an int argument. The call provides no such argument and so is in error.
Overloaded FunctionsAs with any other function, a member function (virtual or otherwise) can be over-loaded. A derived class can redefine zero or more of the versions it inherits.
If a derived class wants to make all the overloaded versions available through its type, then it must either redefine all of them or none of them. Sometimes a class needs to redefine the behavior of only some of the versions in an overloaded set, and wants to inherit the meaning for others. It would be tedious in such cases to have to redefine every base-class version in order to redefine the ones that the class needs to specialize. Instead of redefining every base-class version that it inherits, a derived class can provide a using declaration (Section 15.2.5, p. 574) 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 name 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 need redefine only those functions that it truly must define for its type. It can use the inherited definitions for the others. 15.5.4. Virtual Functions and ScopeRecall that to obtain dynamic binding, we must call a virtual member through a reference or a pointer to a base class. When we do so, the compiler looks for the function in the base class. Assuming the name is found, the compiler checks that the arguments match the parameters. We can now understand why virtual functions must have the same prototype in the base and derived classes. If the base member took different arguments than the derived-class member, there would be no way to call the derived function from a reference or pointer to the base type. Consider the following (artificial) collection of classes: class Base { public: virtual int fcn(); }; class D1 : public Base { public: // hides fcn in the base; this fcn is not virtual int fcn(int); // parameter list differs from fcn in Base // D1 inherits definition of Base::fcn() }; class D2 : public D1 { public: int fcn(int); // nonvirtual function hides D1::fcn(int) int fcn(); // redefines virtual fcn from Base }; The version of fcn in D1 does not redefine the virtual fcn from Base. Instead, it hides fcn from the base. Effectively, D1 has two functions named fcn: The class inherits a virtual named fcn from the Base and defines its own, nonvirtual member named fcn that takes an int parameter. However, the virtual from the Base cannot be called from a D1 object (or reference or pointer to D1) because that function is hidden by the definition of fcn(int). The class D2 redefines both functions that it inherits. It redefines the virtual version of fcn originally defined in Base and the nonvirtual defined in D1. Calling a Hidden Virtual through the Base ClassWhen we call a function through a base-type reference or pointer, the compiler looks for that function in the base class and ignores the derived classes: Base bobj; D1 d1obj; D2 d2obj; Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj; bp1->fcn(); // ok: virtual call, will call Base::fcnat run time bp2->fcn(); // ok: virtual call, will call Base::fcnat run time bp3->fcn(); // ok: virtual call, will call D2::fcnat run time All three pointers are pointers to the base type, so all three calls are resolved by looking in Base to see if fcn is defined. It is, so the calls are legal. Next, because fcn is virtual, the compiler generates code to make the call at run time based on the actual type of the object to which the reference or pointer is bound. In the case of bp2, the underlying object is a D1. That class did not redefine the virtual version of fcn that takes no arguments. The call through bp2 is made (at run time) to the version defined in Base.
![]() |