I l@ve RuBoard Previous Section Next Section

Solution

graphics/bulb_icon.gif

First, let's consider some style issues, and one real error.

  1. "void main()" is nonstandard and therefore nonportable.

    
    
    void main() 
    
    
    

    Yes, alas, I know that this appears in many books. Some authors even argue that "void main()" might be standard-conforming. It isn't. It never was, not even in the 1970s, in the original pre-standard C.

    Even though "void main()" is not one of the legal declarations of main, many compilers allow it. What this means, however, is that even if you are able to write "void main()" today on your current compiler, you may not be able to write it when you port to a new compiler. It's best to get into the habit of using either of the two standard and portable declarations of main:

    
    
    int main() 
    
    
    int main( int argc, char* argv[] )
    
    
    

    You might also have noticed that the problem program does not have a return statement in main. Even with a standard main() that returns an int, that's not a problem (though it's certainly good style to report errors to outside callers), because if main() has no return statement, the effect is that of executing "return 0;". Caveat: As of this writing, many compilers still do not implement this rule and will emit warnings if you don't put an explicit return statement in main().

  2. "delete pb;" is unsafe.

    
    
    delete pb; 
    
    
    

    This looks innocuous. It would be innocuous, if only the writer of Base had supplied a virtual destructor. As it is, deleting via a pointer-to-base without a virtual destructor is evil, pure and simple, and corruption is the best thing you can hope for, because the wrong destructor will get called and operator delete() will be invoked with the wrong object size.

    Guideline

    graphics/guideline_icon.gif

    Make base class destructors virtual (unless you are certain that no one will ever attempt to delete a derived object through a pointer to base).


    For the next few points, it's important to differentiate between three common terms:

    • To overload a function f() means to provide another function with the same name (f) in the same scope but with different parameter types. When f() is actually called, the compiler will try to pick the best match based on the actual parameters that are supplied.

    • To override a virtual function f() means to provide another function with the same name (f) and the same parameter types in a derived class.

    • To hide a function f() in an enclosing scope (base class, outer class, or namespace) means to provide another function with the same name (f) in an inner scope (derived class, nested class, or namespace), which will hide the same function name in an enclosing scope.

  3. Derived::f is not an overload.

    
    
    void Derived::f( complex<double> ) 
    
    
    

    Derived does not overload Base::f, it hides them. This distinction is very important, because it means that Base::f(int) and Base::f(double) are not visible in the scope of Derived. (Amazingly, certain popular compilers do not even emit a warning for this, so it's all the more important that you know about it.)

    If the author of Derived intended to hide the Base functions named f, then this is all right.[2] Usually, however, the hiding is inadvertent and surprising, and the correct way to bring the names into the scope of Derived is to write the using-declaration "using Base::f;".

    [2] When deliberately hiding a base name, it's clearer to do it by writing a private using declaration for the name.

    Guideline

    graphics/guideline_icon.gif

    When providing a function with the same name as an inherited function, be sure to bring the inherited functions into scope with a using-declaration if you don't want to hide them.


  4. Derived::g overrides Base::g but changes the default parameter.

    
    
    void g( int i = 20 ) 
    
    
    

    This is decidedly user-unfriendly. Unless you're really out to confuse people, don't change the default parameters of the inherited functions you override. (In general, it's not a bad idea to prefer overloading to parameter defaulting anyway, but that's a subject in itself.) Yes, this is legal C++; and yes, the result is well-defined; and no, don't do it.

    Further on, we'll see how this can really confuse people.

Guideline

graphics/guideline_icon.gif

Never change the default parameters of overridden inherited functions.


Now that we have those issues out of the way, let's look at the mainline and see whether it does what the programmer intended.



void main() 


{


  Base    b;


  Derived d;


  Base*   pb = new Derived;





  b.f(1.0);


No problem. This first call invokes Base::f( double ), as expected.



d.f(1.0); 


This calls Derived::f( complex<double> ). Why? Well, remember that Derived doesn't declare "using Base::f;" to bring the Base functions named f into scope, and so, clearly, Base::f( int ) and Base::f( double ) can't be called. They are not present in the same scope as Derived::f( complex<double> ) so as to participate in overloading.

The programmer may have expected this to call Base::f( double ), but in this case there won't even be a compile error, because, fortunately(?), complex<double> provides an implicit conversion from double, so the compiler interprets this call to mean Derived::f( complex<double>(1.0) ).



pb->f(1.0); 


Interestingly, even though the Base* pb is pointing to a Derived object, this calls Base::f( double ), because overload resolution is done on the static type (here Base), not the dynamic type (here Derived).

For the same reason, the call "pb->f(complex<double>(1.0));" would not compile, because there is no satisfactory function in the Base interface.



b.g(); 


This prints "10", because it simply invokes Base::g( int ) whose parameter defaults to the value 10. No sweat.



d.g(); 


This prints "Derived::g() 20", because it simply invokes Derived::g( int ) whose parameter defaults to the value 20. Also no sweat.



pb->g(); 


This prints "Derived::g() 10".

"Wait a minute," you protest, "what's going on here?" This result may temporarily lock your mental brakes and bring you to a screeching halt until you realize that what the compiler has done is quite proper.[3] The thing to remember is that, like overloads, default parameters are taken from the static type (here Base) of the object, hence the default value of 10 is taken. However, the function happens to be virtual, so the function actually called is based on the dynamic type (here Derived) of the object.

[3] Although, of course, the programmer of Derived ought to be taken out into the back parking lot and yelled at.

If you understand the last few paragraphs about name hiding and when static versus dynamic types are used, then you understand this stuff cold. Congratulations!



  delete pb; 


}


Finally, as noted, the delete will of course corrupt your memory anyway and leave things partially destroyed; see the part about virtual destructors above.

    I l@ve RuBoard Previous Section Next Section