GotW #5

Home Blog Talks Books & Articles Training & Consulting

On the
blog
RSS feed November 4: Other Concurrency Sessions at PDC
November 3
: PDC'09: Tutorial & Panel
October 26: Hoare on Testing
October 23
: Deprecating export Considered for ISO C++0x

This is the original GotW problem and solution substantially as posted to Usenet. See the book Exceptional C++ (Addison-Wesley, 2000) for the most current solutions to GotW issues #1-30. The solutions in the book have been revised and expanded since their initial appearance in GotW. The book versions also incorporate corrections, new material, and conformance to the final ANSI/ISO C++ standard.

Overriding Virtual Functions
Difficulty: 6 / 10

Virtual functions are a pretty basic feature, right? If you can answer questions like this one, then you know them cold.

Problem

In your travels through the dusty corners of your company's code archives, you come across the following program fragment written by an unknown programmer. The programmer seems to have been experimenting to see how some C++ features worked. What did the programmer probably expect the program to print, but what is the actual result?

    #include <iostream>
    #include <complex>
    using namespace std;

    class Base {
    public:
        virtual void f( int ) {
            cout << "Base::f(int)" << endl;
        }

        virtual void f( double ) {
            cout << "Base::f(double)" << endl;
        }

        virtual void g( int i = 10 ) {
            cout << i << endl;
        }
    };

    class Derived: public Base {
    public:
        void f( complex<double> ) {
            cout << "Derived::f(complex)" << endl;
        }

        void g( int i = 20 ) {
            cout << "Derived::g() " << i << endl;
        }
    };

    void main() {
        Base    b;
        Derived d;
        Base*   pb = new Derived;

        b.f(1.0);
        d.f(1.0);
        pb->f(1.0);

        b.g();
        d.g();
        pb->g();

        delete pb;
    }

Solution

First, some style issues:

1. void main()

This is not one of the legal declarations of main, although many compilers will allow it. Use either "int main()" or "int main( int argc, char* argv[] )".

However, note that you still don't need a return statement (though it's good style to report errors to outside callers!)... if main has no return statement, the effect is that of executing "return 0;".

2. delete pb;

This looks innocuous, and it would be if 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.

[RULE] Make base class destructors virtual.

3. 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 Derived! (Note that certain popular compilers do not even emit a warning for this.)

[RULE] 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( int i = 10 )

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. See below for how this can really confuse people.

[RULE] Never change the default parameters of overridden inherited functions.

Now that we have the major style issues out of the way, let's look at the mainline and see whether it does that the programmer intended:

    void main() {
        Base    b;
        Derived d;
        Base*   pb = new Derived;

        b.f(1.0);

No problem. Calls Base::f( double ).

        d.f(1.0);

This calls Derived::f( complex<double> ). Why? Well, remember that Derived doesn't declare "using Base:f;", and so clearly Base::f( int ) and Base::f( double ) can't be called.

The programmer may have expected it to call the latter, but in this case won't even get a compile error since fortunately(?) complex<double> has an implicit(*) conversion from double, and so the compiler sees this as Derived::f( complex<double>(1.0) ).

(*) as of the current draft, the conversion ctor is not explicit

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

        b.g();

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

        d.g();

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

        pb->g();

This prints "Derived::g() 10"... which may temporarily lock your mental brakes and bring you to a screeching halt until you realise 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, and so the function actually called is based on the dynamic type (here Derived) of the object.

(**) although of course the progammer ought to be shot

If you understand the last few paragraphs (since "Oops!"), then you understand this stuff cold. Congratulations!

        delete pb;
    }

And the delete, of course, will corrupt your memory anyway and leave things partially destroyed... see the part about virtual dtors above.

Copyright © 2009 Herb Sutter