Solution

I l @ ve RuBoard

graphics/bulb.gif

1. What is a pure virtual function? Give an example.

A pure virtual function is a virtual function you want to force concrete derived classes to override. If a class has any non-overridden pure virtuals, it is an "abstract" class, and you can't create objects of that type.

 // Example 27-1 // class AbstractClass {   // declare a pure virtual function:   // this class is now abstract   virtual void f(int) = 0; }; class StillAbstract : public AbstractClass {   // does not override f(int),   // so this class is still abstract }; class Concrete : public StillAbstract { public:   // finally overrides f(int),   // so this class is concrete   void f(int) { /*...*/ } }; AbstractClass a;    // error, abstract class StillAbstract b;    // error, abstract class Concrete      c;    // ok, concrete class 

2. Why might you declare a pure virtual function and also write a definition (body)? Give as many possible reasons or situations as you can.

Let's consider the three main reasons you might do this. Of these three reasons, #1 is commonplace, #2 and #3 are useful but somewhat more rare, and #4 is a workaround used occasionally by advanced programmers who are working with weaker compilers. (Of course we need a fourth entry in a three-reason list, for the self-evident reason that no trilogy is complete without a fourth item. [2] )

[2] "Our main weapon is surprise. Fear and surprise are our two main weapons." (Monty Python's Flying Circus)

1. Pure Virtual Destructor

All base class destructors should be either virtual and public, or nonvirtual and protected. In brief, here's the reason: First, recall that you should always avoid deriving from a concrete class. Assuming that it's given that the base class is therefore not concrete, and therefore doesn't necessarily need a public destructor for the purpose of being instantiated on its own, you're left with one of two situations. Either (a) you want to allow polymorphic deletion through a base pointer, in which case the destructor must be virtual and public; or (b) you don't, in which case the destructor should be nonvirtual and protected, the latter to prevent the unwanted usage. For more, see [Sutter01].

If the class should be abstract (you want to prevent instantiating it) but it doesn't have any other pure virtual functions and it has a public destructor, the following is a common technique to make the destructor pure virtual (it should be anyway).

 // Example 27-2(a) // // file b.h // class B { public: /*...other stuff...*/   virtual ~B() = 0; // pure virtual destructor }; 

Of course, any derived class's destructor must implicitly call the base class's destructor, so the destructor must still be defined (even if it's empty).

 // Example 27-2(a), continued // // file b.cpp // 
 B::~B() { /* possibly empty */ } 

If this definition were not supplied, you could still derive other classes from B , but they could never be instantiated, which makes them not particularly useful.

Guideline

graphics/guideline.gif

Always make base class destructors virtual and public or nonvirtual and protected.


2. Force Conscious Acceptance of Default Behavior

If a derived class doesn't choose to override a normal virtual function, it just inherits the base version's behavior by default. If you want to provide a default behavior but not let derived classes just inherit it "silently" like this, you can make it pure virtual and still provide a default the derived class author has to call deliberately if he wants it.

 // Example 27-2(b) // class B { protected:   virtual bool f() = 0; }; bool B::f() {   return true;   // this is a good default, but }                  // shouldn't be used blindly class D : public B {   bool f()   {     return B::f(); // if D wants the default   }                    // behaviour, it has to say so }; 

In the "gang of four" book Design Patterns [Gamma95], the State pattern demonstrates one example of how this technique can be put to good use.

3. Provide Partial Behavior

It's often useful to provide partial behavior to a derived class which the derived class must still complete. The idea is that the derived class executes the base class's implementation as part of its own implementation:

 // Example 27-2(c) // class B {   // ... protected virtual bool f() = 0; }; bool B::f() {   // do something general-purpose } class D : public B {   bool f()   {     // first, use the base class's implementation     B::f();     // ... now do more work ...   } }; 

Referring again to [Gamma95], the Decorator pattern demonstrates a good use for this technique.

4. Work Around Poor Compiler Diagnostics

There are situations in which you could accidentally end up calling a pure virtual function (indirectly from a base constructor or destructor; see your favorite advanced C++ book for examples). Of course, well-written code normally won't get into these problems, but no one's perfect, and once in a while it happens.

Unfortunately, not all compilers [3] actually tell you when this is the problem. Those that don't can give you spurious unrelated errors that take forever to track down. "Argh," you scream when you finally diagnose the problem some hours later, "why didn't the compiler just tell me that's what I did?!" (Answer: Because that's one manifestation of "undefined behavior.")

[3] Well, technically it's the runtime environment that catches this sort of thing. I'll say compiler anyway, because it's generally the compiler that ought to slip in the code that checks this for you at runtime.

One way to protect yourself against this wasted debugging time is to provide definitions for the pure virtual functions that should never be called, and put some really evil code into those definitions, which lets you know right away if you call them accidentally.

For example:

 // Example 27-2(d) // class B { public:   bool f();      // possibly instrumented, calls do_f()                  // (Non-Virtual Interface pattern) private:   virtual bool do_f() = 0; }; bool B::do_f()   // this should NEVER be called {   if( PromptUser( "pure virtual B::f called -- "                   "abort or ignore?" ) == Abort )     DieDieDie(); } 

In the common DieDieDie() function, do whatever is necessary on your system to get into the debugger or dump a stack trace or otherwise get diagnostic information. Here are some common methods that will get you into the debugger on most systems. Pick the one you like best.

 void DieDieDie()  // a C way to scribble through a null {                 // pointer... a proven crowd-pleaser   memset( 0, 1, 1 ); } void DieDieDie()  // another C-style method {   abort(); } void DieDieDie()  // a C++ way to scribble through a null                   // data pointer   *static_cast<char *>(0) = 0; } void DieDieDie()  // a C++ way to follow a null function                   // pointer to code that doesn't exist   static_cast<void(*)()>(0)(); } void DieDieDie()  // unwind back to last "catch(...)" {   class LocalClass {};   throw LocalClass(); } void DieDieDie()  // an alternative for standardistes {   throw std::logic_error(); }                  // for standardistes having good compilers void DieDieDie() throw() {   throw 0; } 

You get the idea. Be creative. There's no shortage of ways to deliberately crash a program, but the idea is to do it in a way that will cause your runtime debugger to place you as close to the point of failure as possible. Scribbling through a null pointer is one of the bets that pays most consistently.

I l @ ve RuBoard


More Exceptional C++
More Exceptional C++: 40 New Engineering Puzzles, Programming Problems, and Solutions
ISBN: 020170434X
EAN: 2147483647
Year: 2001
Pages: 118
Authors: Herb Sutter

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net