[ Team LiB ] Previous Section Next Section

Gotcha #74: Virtual Functions with Default Argument Initializers

This is really the same problem as overloading virtual functions. Like overloading, default argument initializers are basically syntactic sugar, used to change the interface of a function without adding new behavior:



class Thing { 


   // . . .


   virtual void doitNtimes( int numTimes = 12 );


};


class MyThing : public Thing {


   // . . .


   void doitNtimes( int numTimes = 10 );


};


The problems that arise result from a mismatch between the static and dynamic behavior of an object and are often difficult to track down:



Thing *t = new MyThing; 


t->doitNtimes();


The assumption is that it's important to do it, by default, ten times for a MyThing versus twelve times for other types of Thing. Unfortunately, the default argument initializer is applied statically, and the statically determined base class default of 12 is passed to the dynamically bound derived class function.

We could attempt to circumvent this problem by demanding that all derived class designers duplicate precisely a base class function's default initializers when overriding. However, this is a bad approach, for a number of reasons.

First, developers being the way they are, some of them won't follow this advice. (They may have lost confidence in the base class design when they saw the default argument initializer, and decided to strike out on their own.)

Second, such advice renders the derived classes unnecessarily vulnerable to changes in the base class. If the default argument initializer should be modified in the base class, that would require a coordinated change to every derived class as well. Typically, this is not possible.

Third, the meaning of a default argument initializer can vary, according to where it appears in the source code. Syntactically identical initializers may have very different meanings between the base class and derived class contexts:



// In file thing.h . . . 


const int minim = 12;


namespace SCI {


class Thing {


   // . . .


   virtual void doitNtimes( int numTimes = minim ); // uses ::minim


};


}





// In file mything.h . . .


namespace SCI {


const int minim = 10;


class MyThing : public Thing {


   // . . .


   void doitNtimes( int numTimes = minim ); // uses SCI::minim


};


}


It would be hard to blame the derived class designer for picking up the wrong minim, particularly if the declaration of SCI::minim were added after the MyThing class had been written.

The safest and simplest procedure is to avoid default argument initializers in virtual functions. As with overloaded virtual functions, we can achieve our interface goals with a little inline trickery:



class Thing { 


   // . . .


   void doitNtimes( int numTimes = minim )


       { doitNtimesImpl( numTimes ); }


 protected:


   virtual void doitNtimesImpl( int numTimes );


};


Users of the Thing hierarchy will pick up the statically determined default from the base class interface, and derived classes can modify the behavior of the function without concerning themselves with a default initializer.

    [ Team LiB ] Previous Section Next Section