I l@ve RuBoard |
![]() ![]() |
5.5 Defining a Derived ClassIThe derived class consists of two parts: the subobject of its base class (consisting of the nonstatic base class data members, if any) and the derived class portion (consisting of the nonstatic derived class data members). (Think of a blue Lego block snapped together with a red one.) This composite nature of the derived class is reflected in its declaration syntax: // the header file contains the base class definition #include "num_sequence.h" class Fibonacci : public num_sequence { public: // ... }; The derived class name is followed by a colon, the public keyword, and the name of the base class. [3] The only rule is that the base class definition must be present before a class can inherit from it (this is why the header file containing the num_sequence class definition is included).
The Fibonacci class must provide an implementation of each of the pure virtual functions inherited from its base class. In addition, it must declare those members that are unique to the Fibonacci class. Here is the class definition: class Fibonacci : public num_sequence { public: Fibonacci( int len = 1, int beg_pos = 1 ) : _length( len ), _beg_pos( beg_pos ){} virtual int elem( int pos ) const; virtual const char* what_am_i() const { return "Fibonacci"; } virtual ostream& print( ostream &os = cout ) const; int length() const { return _length; } int beg_pos() const { return _beg_pos; } protected: virtual void gen_elems( int pos ) const; int _length; int _beg_pos; static vector<int> _elems; }; In this design, length and beginning position are data members of each derived class. The read access functions length() and beg_pos() are declared as nonvirtual because there is no base class instance to override. Because they are not part of the base class interface, they cannot be accessed when we're programming through a base class pointer or reference. For example, num_sequence *ps = new Fibonacci( 12, 8 ); // ok: invokes Fibonacci::what_am_i() through virtual mechanism ps->what_am_i(); // ok: invokes inherited num_sequence::max_elems(); ps->max_elems(); // error: length() is not part of num_sequence interface ps->length(); // ok: invokes Fibonacci destructor through virtual mechanism delete ps; If the inaccessibility of length() and beg_pos() through the base class interface turns out to be a problem for our users, we'll need to go back and modify the base class interface. One redesign is to introduce length() and beg_pos() as pure virtual functions within the num_sequence base class. This automatically turns the derived class instances of beg_pos() and length() into virtual functions. This is one reason that the derived class instances of a virtual function are not required to specify the virtual keyword. If the keyword were required, retrofitting a base class virtual function such as beg_pos() would be difficult to get right: Every derived class instance would need to be redeclared. An alternative redesign might be to factor the storage of the length and beginning position from the derived classes into the base class. In this way, length() and beg_pos() become inherited inline nonvirtual functions. (We consider the ramifications of this design in Section 5.6.) My point in bringing this up is that the challenge of an object-oriented design is not so much in the programming as it is in the factoring of the base and derived classes and determining the interface and members that belong to each. In general, this is an iterative process that evolves through experience and feedback from users. Here is the implementation of elem(). The derived class virtual function must exactly match the function prototype of the base class instance. The virtual keyword is not specified in a definition that occurs outside the class. int Fibonacci:: elem( int pos ) const { if ( ! check_integrity( pos )) return 0; if ( pos > _elems.size() ) Fibonacci::gen_elems( pos ); return _elems[ pos-1 ]; } Notice that elem() invokes the inherited member check_integrity() exactly as if it were a member of its class. In general, the inherited public and protected base class members, regardless of the depth of an inheritance hierarchy, are accessed as if they are members of the derived class. The public base class members are also public in the derived class and are available to users of the derived class. The protected base class members are protected within the derived class. They are available to classes inheriting from the derived class but not to users of the class. The derived class, however, has no access privilege to the private base class members. Notice that before the element at pos is returned, a check is made whether _elems holds sufficient elements. If it does not, elem() invokes gen_elems() to fill _elems up to pos. The invocation is written as Fibonacci::gen_elems(pos) rather than the simpler gen_elems(pos). A good question might be, why? Within elem(), we know exactly which instance of gen_elems() we want to invoke. Inside the Fibonacci instance of elem(), we want to invoke the Fibonacci instance of gen_elems(). Delaying the resolution of gen_elems() until run-time for this invocation is unnecessary. In effect, we'd like to override the virtual mechanism and have the function resolved at compile-time rather than run-time. This is what the explicit invo-cation of gen_elems() does. By qualifying the call of a virtual function with the class scope operator, we are telling the compiler which instance to invoke. The run-time virtual mechanism is overridden. Here are the implementations of gen_elems() and print(): void Fibonacci:: gen_elems( int pos ) const { if ( _elems.empty() ) { _elems.push_back( 1 ); _elems.push_back( 1 ); } if ( _elems.size() < pos ) { int ix = _elems.size(); int n_2 = _elems[ ix-2 ]; int n_1 = _elems[ ix-1 ]; for ( ; ix < pos; ++ix ) { int elem = n_2 + n_1; _elems.push_back( elem ); n_2 = n_1; n_1 = elem; } } } ostream& Fibonacci:: print( ostream &os ) const { int elem_pos = _beg_pos-1; int end_pos = elem_pos + _length; if ( end_pos > _elems.size() ) Fibonacci::gen_elems( end_pos ); while ( elem_pos < end_pos ) os << _elems[ elem_pos++ ] << ' '; return os; } Notice that both elem() and print() check that _elems contains sufficient elements, and, if it does not, they invoke gen_elems(). How might we retrofit check_integrity() to make that test as well as check the validity of pos? One possibility is to provide the Fibonacci class with a check_integrity() member function: class Fibonacci : public num_sequence { public: // ... protected: bool check_integrity( int pos ) const; // ... }; Within the Fibonacci class, every reference to check_integrity() now resolves to the derived class instance of the function. Within elem(), for example, the call of check_integrity() now invokes the Fibonacci member. int Fibonacci:: elem( int pos ) const { // now resolves to Fibonacci's instance if ( ! check_integrity( pos )) return 0; // ... } Whenever a member of the derived class reuses the name of an inherited base class member, the base class member becomes lexically hidden within the derived class. That is, each use of the name within the derived class resolves to the derived class member. To access the base class member within the derived class, we must qualify its reference with the class scope operator of the base class. For example, inline bool Fibonacci:: check_integrity( int pos ) const { // class scope operator necessary ... // unqualified name resolves to this instance! if ( ! num_sequence::check_integrity( pos )) return false; if ( pos > _elems.size() ) Fibonacci::gen_elems( pos ); return true; } The problem with this solution is that, within the base class, check_integrity() is not identified as virtual. This means that each invocation of check_integrity() through a base class pointer or reference resolves to the num_sequence instance. There is no consideration taken of the actual object addressed. For example, void Fibonacci::example() { num_sequence *ps = new Fibonacci( 12, 8 ); // ok: resolves to Fibonacci::elem() through virtual mechanism ps->elem( 1024 ); // oops: resolves statically to num_sequence::check_integrity() // based on the type of ps ps->check_integrity( pos ); } For this reason, it is not, in general, a good practice to provide nonvirtual member functions with the same name in both the base and the derived class. One conclusion to draw from this might be that all functions within the base class should be declared as virtual. I don't believe this is the correct conclusion, but it does solve the immediate dilemma of our design. The underlying cause of the dilemma is that the base class instance has been implemented without adequate knowledge of what the derived classes require to check the integrity of their state. Any implementation that proceeds from insufficient knowledge is likely to prove incomplete. But this is different from claiming that the implementation is type-dependent and therefore must be virtual. Again, the point is that our designs are iterative and must evolve through experience and feedback from users. In this case, the better design solution is to redefine check_integrity() to take two parameters: bool num_sequence:: check_integrity( int pos, int size ) { if ( pos <= 0 || pos > max_seq ){ // same as before ... } if ( pos > size ) // gen_elems() is invoked through virtual mechanism gen_elems( pos ); return true; } In this definition of check_integrity(), gen_elems() is invoked through the virtual mechanism. If check_integrity() is invoked by a Fibonacci class object, the Fibonacci gen_elems() instance is invoked. If check_integrity() is invoked by a Triangular class object, the Triangular gen_elems() instance is invoked, and so on. The new instance might be invoked as follows: int Fibonacci:: elem( int pos ) { if ( ! check_integrity( pos, _elems.size() )) return 0; // ... } IIt is always a good idea to test an implementation incrementally rather than wait until the entire code base is complete to see whether the darn thing works. Not only does this allow us a sanity check as we proceed, but it also provides the basis for a suite of regression tests that we can run each time we subsequently evolve the design. Here's a small test program to exercise our implementation so far. gen_elems() has been instrumented to display the elements it generates other than the first two: int main() { Fibonacci fib; cout << "fib: beginning at element 1 for 1 element: " << fib << endl; Fibonacci fib2( 16 ); cout << "fib2: beginning at element 1 for 16 elements: " << fib2 << endl; Fibonacci fib3( 8, 12 ); cout << "fib3: beginning at element 12 for 8 elements: " << fib3 << endl; } When the program is compiled and executed, it generates the following output: fib: beginning at element 1 for 1 element: ( 1 , 1 ) 1 fib2: beginning at element 1 for 16 elements: gen_elems: 2 gen_elems: 3 gen_elems: 5 gen_elems: 8 gen_elems: 13 gen_elems: 21 gen_elems: 34 gen_elems: 55 gen_elems: 89 gen_elems: 144 gen_elems: 233 gen_elems: 377 gen_elems: 610 gen_elems: 987 ( 1 , 16 ) 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 fib3: beginning at element 12 for 8 elements: gen_elems: 1597 gen_elems: 2584 gen_elems: 4181 ( 12 , 8 ) 144 233 377 610 987 1597 2584 4181 ![]() |
I l@ve RuBoard |
![]() ![]() |