Solution
First, let's consider some style issues, and one
real error.
-
"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().
-
"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
|
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.
-
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. 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;".
Guideline
|
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.
|
-
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
|
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. 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.
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.
|