Section 15.5. Class Scope under Inheritance


15.5. Class Scope under Inheritance

Each 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:

  1. bulk is an object of the Bulk_item class. The Bulk_item class is searched for book. That name is not found.

  2. Because Bulk_item is derived from Item_Base, the Item_Base class is searched next. The name book is found in the Item_base class. The reference is resolved successfully.

15.5.1. Name Lookup Happens at Compile Time

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

Exercises Section 15.5.1

Exercise 15.21:

Redefine your Item_base hierarchy to include a Disc_item class.

Exercise 15.22:

Redefine Bulk_item and the class you implemented in the exercises from Section 15.2.3 (p. 567) that represents a limited discount strategy to inherit from Disc_item.


15.5.2. Name Collisions and Inheritance

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

A derived-class member with the same name as a member of the base class hides direct access to the base-class member.



      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 Members

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

When designing a derived class, it is best to avoid name collisions with members of the base class whenever possible.



Exercises Section 15.5.2

Exercise 15.23:

Given the following base- and derived-class definitions

      struct Base {          foo(int);      protected:          int bar;          double foo_bar;      };      struct Derived : public Base {          foo(string);          bool bar(Base *pb);          void foobar();      protected:          string bar;      }; 

identify the errors in each of the following examples and how each might be fixed:

      (a) Derived d; d.foo(1024);      (b) void Derived::foobar() { bar = 1024; }      (c) bool Derived::bar(Base *pb)               { return foo_bar == pb->foo_bar; } 


15.5.3. Scope and Member Functions

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

Recall that functions declared in a local scope do not overload functions defined at global scope (Section 7.8.1, p. 268). Similarly, functions defined in a derived class do not overload members defined in the base. When the function is called through a derived object, the arguments must match a version of the function defined in the derived class. The base class functions are considered only if the derived does not define the function at all.



Overloaded Functions

As 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 the derived class redefines any of the overloaded members, then only the one(s) redefined in the derived class are accessible through the derived type.



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 Scope

Recall 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 Class

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

Key Concept: Name Lookup and Inheritance

Understanding how function calls are resolved is crucial to understanding inheritance hierarchies in C++. The following four steps are followed:

1.

Start by determining the static type of the object, reference, or pointer through which the function is called.

2.

Look for the function in that class. If it is not found, look in the immediate base class and continue up the chain of classes until either the function is found or the last class is searched. If the name is not found in the class or its enclosing base classes, then the call is in error.

3.

Once the name is found, do normal type-checking (Section 7.1.2, p. 229) to see if this call is legal given the definition that was found.

4.

Assuming the call is legal, the compiler generates code. If the function is virtual and the call is through a reference or pointer, then the compiler generates code to determine which version to run based on the dynamic type of the object. Otherwise, the compiler generates code to call the function directly.




C++ Primer
C Primer Plus (5th Edition)
ISBN: 0672326965
EAN: 2147483647
Year: 2006
Pages: 223
Authors: Stephen Prata

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net