Previous section   Next section

Imperfect C++ Practical Solutions for Real-Life Programming
By Matthew Wilson
Table of Contents
Chapter 22.  Bolt-ins


22.3. Nonvirtual Overriding

When writing bolt-ins (and veneers also, on occasion) that are aimed at a general model of primary parameterizing type, it can be the case that you may be hiding, rather than overriding, for some of the targeted types. Overriding nonvirtual functions is usually a much frowned-upon activity, and quite rightly, too. Doing so can cause the instance of a class to have different behavior depending on where it is used. Consider the tradingRegister class in Listing 22.1, which has not been designed to act as a base class because it's Add() method is nonvirtual.

Listing 22.1.


class TradingRegister


{


public:


  void Add(Trade const &trade); // Adds a trade to the trading reg


};





void DoTrade(TradingRegister &reg, Trade const &trade)


{


  reg.Add(trade);


}



Subsequently, someone has need to override the functionality provided in the guise of the LoggingTradingRegister, shown in Listing 22.2. Unfortunately, passing the LoggingTradingRegister instance to DoTrade() causes an error, in that the Add() method from tradingRegister is the one that is called.

Listing 22.2.


class LoggingTradingRegistr


  : public TradingRegistr


{


public:


  void Add(Trade const &trade); // Adds a trade and logs it


};





Trade                   trade;


LoggingTradingRegister  ltr;





DoTrade(ltr, trade); // Nasty. Trade doesn't get logged



This is the result of C++'s type resolution and inheritance mechanisms working together to be able to interpret instances of a type as that type, or as any of its publicly derived parent types. Without these mechanisms, C++ would be a pretty useless language, but it does mean that we should avoid overriding nonvirtual mechanisms in general.

However, where there is no chance of the type being used via its parent type, there is no problem with non-virtual overriding. This is the case when using templates. Consider if DoTrade()was expressed as a template:



template< typename T


        , typename U


        >


void DoTrade(T &reg, U const &trade)


{


  reg.Add(trade);


}


. . .





DoTrade(ltr, trade); // Fine. Trade is logged



This form of DoTrade() does not, and in fact cannot, know about the parent class(es) of T. Hence, the instantiation of DoTrade()knows only that ltr is of type LoggingTradingRegister, so it can only call LoggingTradingRegister::Add().

Although using them with templates is safe, types that employ nonvirtual overriding can also be used in other nontemplate contexts, in which case they still represent a danger. The problem with any technique that employs template derivation—whether that be veneers or bolt-ins or any similar mechanisms—is that experience shows that it can be very easy to break the rule against hiding when you're designing templates with a large potential set of parameterizing types. This is a significant flaw in these techniques, and one must be mindful of it when using them. In practice, users must treat such components as a white box just as they must when using libraries based on run time polymorphism.[2]

[2] The current (2003) intensity of the debate regarding implementing the export keyword seems to miss this point. Anachronistically, my opinion is that the only components that even approximate a black box are those hidden behind a compiler-independent library C-API.


      Previous section   Next section