(Im)pure Virtual Functions

Does it ever make sense to make a function pure virtual, but still provide a body?

Problem

JG Question

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

Guru Question

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

Solution

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

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

class AbstractClass { public: // 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 reasons or situations as you can.

There are three main reasons you might do this. #1 is commonplace, #2 is pretty rare, and #3 is a workaround used occasionally by advanced programmers working with weaker compilers.

Most programmers should only ever use #1.

1. Pure Virtual Destructor

All base classes should have a virtual destructor (see your favourite C++ book for the reasons). If the class should be abstract (you want to prevent instantiating it) but it doesn't happen to have any other pure virtual functions, a common technique to make the destructor pure virtual:

// file b.h class B { public: /*...other stuff...*/ virtual ~B() = 0; // pure virtual dtor };

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

// file b.cpp B::~B() { /* possibly empty */ }

If this definition were not supplied, you could still derive classes from B but they could never be instantiated, which isn't particularly useful.

2. Force Conscious Acceptance of Default Behaviour

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

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

3. Workaround Poor Compiler Diagnostics

There are situations where you could accidentally end up calling a pure virtual function (indirectly from a base constructor or destructor; see your favourite 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[1] will 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 do finally diagnose the problem yourself some hours later, "why didn't the compiler just tell me that's what I did?!"

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 that lets you know right away if you do call them accidentally. For example:

class B { public: virtual bool f() = 0; }; bool B::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 that you like best.

void DieDieDie() { // scribble through null ptr memset( 0, 1, 1 ); } void DieDieDie() { // another C-style method assert( false ); } void DieDieDie() { // back to last "catch(...)" class LocalClass {}; throw LocalClass(); } void DieDieDie() { // for standardphiles throw logic_error(); }

You get the idea. Be creative. :-)