It is the way the classes are constructed and destructed.
Base is first constructed, then Derived. So in the constructor of Base, Derived has not yet been created. Therefore none of its member functions can be called. So if the constructor of Base calls a virtual function, it can't be the implementation from Derived, it must be the one from Base. But the function in Base is pure virtual and there is nothing to call.
In destruction, first Derived is destroyed, then Base. So once again in the destructor of Base there is no object of Derived to invoke the function, only Base.
Incidentally it is only undefined where the function is still pure virtual. So this is well-defined:
struct Base
{
virtual ~Base() { /* calling foo here would be undefined */}
virtual void foo() = 0;
};
struct Derived : public Base
{
~Derived() { foo(); }
virtual void foo() { }
};
The discussion has moved on to suggest alternatives that:
- It might produce a compiler error, just like trying to create an instance of an abstract class does.
The example code would no doubt be something like:
class Base
{
// other stuff
virtual void init() = 0;
virtual void cleanup() = 0;
};
Base::Base()
{
init(); // pure virtual function
}
Base::~Base()
{
cleanup(); // which is a pure virtual function. You can't do that! shouts the compiler.
}
Here it is clear what you are doing is going to get you into trouble. A good compiler might issue a warning.
- it might produce a link error
The alternative is to look for a definition of Base::init()
and Base::cleanup()
and invoke that if it exists, otherwise invoke a link error, i.e. treat cleanup as non-virtual for the purpose of constructors and destructors.
The issue is that won't work if you have a non-virtual function calling the virtual function.
class Base
{
void init();
void cleanup();
// other stuff. Assume access given as appropriate in examples
virtual ~Base();
virtual void doinit() = 0;
virtual void docleanup() = 0;
};
Base::Base()
{
init(); // non-virtual function
}
Base::~Base()
{
cleanup();
}
void Base::init()
{
doinit();
}
void Base::cleanup()
{
docleanup();
}
This situation looks to me to be beyond the capability of both the compiler and linker. Remember that these definitions could be in any compilation unit. There is nothing illegal about the constructor and destructor calling init() or cleanup() here unless you know what they are going to do, and there is nothing illegal about init() and cleanup() calling the pure virtual functions unless you know from where they are invoked.
It is totally impossible for the compiler or linker to do this.
Therefore the standard must allow the compile and link and mark this as "undefined behaviour".
Of course if an implementation does exist, the compiler is free to use it if able. Undefined behaviour doesn't mean it has to crash. Just that the standard doesn't say it has to use it.
Note that this case the destructor is calling a member function that calls the pure virtual but how do you know it will do even this? It could be calling something in a completely different library that invokes the pure virtual function (assume access is there).
Base::~Base()
{
someCollection.removeMe( this );
}
void CollectionType::removeMe( Base* base )
{
base->cleanup(); // ouch
}
If CollectionType exists in a totally different library there is no way any link error can occur here. The simple matter is again that the combination of these calls is bad (but neither one individually is faulty). If removeMe is going to be calling pure-virtual cleanup() it cannot be called from Base's destructor, and vice-versa.
One final thing you have to remember about Base::init()
and Base::cleanup()
here is that even if they have implementations, they are never called through the virtual function mechanism (v-table). They would only ever be called explicitly (using full class-name qualification) which means that in reality they are not really virtual. That you are allowed to give them implementations is perhaps misleading, probably wasn't really a good idea and if you wanted such a function that could be called through derived classes, perhaps it is better being protected and non-virtual.
Essentially: if you want the function to have the behaviour of a non-pure virtual function, such that you give it an implementation and it gets called in the constructor and destructor phase, then don't define it as pure virtual. Why define it as something you don't want it to be?
If all you want to do is prevent instances being created you can do that in other ways, such as:
- Make the destructor pure virtual.
- Make the constructors all protected