Why a virtual call to a pure virtual function from a constructor is UB and a call to a non-pure virtual function is allowed by the Standard?
Asked Answered
B

4

20

From 10.4 Abstract Classes parag. 6 in the Standard :

"Member functions can be called from a constructor (or destructor) of an abstract class; the effect of making a virtual call to a pure virtual function directly or indirectly for the object being created (or destroyed) from such a constructor (or destructor) is undefined."

Assuming that a call to a non-pure virtual function from a constructor (or destructor), is allowed by the Standard, why the difference ?

[EDIT] More standards quotes about pure virtual functions:

§ 10.4/2 A virtual function is specified pure by using a pure-specifier (9.2) in the function declaration in the class definition. A pure virtual function needs be defined only if called with, or as if with (12.4), the qualified-id syntax (5.1). ... [ Note: A function declaration cannot provide both a pure-specifier and a definition —end note ]

§ 12.4/9 A destructor can be declared virtual (10.3) or pure virtual (10.4); if any objects of that class or any derived class are created in the program, the destructor shall be defined.

Some questions that need answering are:

  • Where the pure virtual function has not been given an implementation, should this not be a compiler or linker error instead?

  • Where the pure virtual function has been given an implementation, why can it not be well-defined in this case to invoke this function?

Baird answered 8/2, 2012 at 0:3 Comment(9)
I've heard this asked before (elsewhere); there didn't seem to be a great reason for it.Venue
possible duplicate of Pure virtual invocation from constructor and destructorVenue
You may call a virtual function that is not pure but it won't call the derived version it will call the version of the class whose constructor/destructor it is.Condor
@CashCow: Right, so the question is, why doesn't invoking a pure virtual member function do the exact same thing?Venue
@Condor Why then, the Standard does not allow the same thing for a "defined" pure virtual function ?Baird
As I said, this debate has been had, and AFAICT there is no satisfactory answer. It's a "it just is" and "might make a compiler's life slightly easier" and "you don't want to do it anyway" sort of story.Venue
A good answer could be "if you want it to act as non-pure then don't define it as pure".Condor
Or "it might make a compiler's life slightly easier if you say what you mean and don't make a function pure virtual when you don't want it to act like one"Condor
Can somebody please put into the question a minimal example of a piece of code that is undefined in this case? Before we argue about why, I want to see a concrete example of what we're talking about. (I think my second answer includes such an example, but I might be wrong.)Dinse
S
16

Because a virtual call can NEVER call a pure virtual function -- the only way to call a pure virtual function is with an explicit (qualified) call.

Now outside of constructors or destructors, this is enforced by the fact that you can never actually have objects of an abstract class. You must instead have an object of some non-abstract derived class which overrides the pure virtual function (if it didn't override it, the class would be abstract). While a constructor or destructor is running, however, you might have an object of an intermediate state. But since the standard says that trying to call a pure virtual function virtually in this state results in undefined behavior, the compiler is free to not have to special case things to get it right, giving much more flexibility for implementing pure virtual functions. In particular, the compiler is free to implement pure virtuals the same way it implements non-pure virtuals (no special case needed), and crash or otherwise fail if you call the pure virtual from a ctor/dtor.

Supernational answered 8/2, 2012 at 0:59 Comment(0)
D
4

I think this code is an example of the undefined behaviour referenced by the standard. In particular, it is not easy for the compiler to notice that this is undefined.

(BTW, when I say 'compiler', I really mean 'compiler and linker'. Apologies for any confusion.)

struct Abstract {
    virtual void pure() = 0;
    virtual void foo() {
        pure();
    }
    Abstract() {
        foo();
    }
    ~Abstract() {
        foo();
    }
};

struct X : public Abstract {
    virtual void pure() { cout << " X :: pure() " << endl; }
    virtual void impure() { cout << " X :: impure() " << endl; }
};
int main() {
    X x;
}

If the constructor of Abstract directly called pure(), this would obviously be a problem and a compiler can easily see that there is no Abstract::pure() to be called, and g++ gives a warning. But in this example, the constructor calls foo(), and foo() is a non-pure virtual function. Therefore, there is no straightforward basis for the compiler or linker to give a warning or error.

As onlookers, we can see that foo is a problem if called from the constructor of Abstract. Abstract::foo() itself is defined, but it tries to call Abstract::pure and this doesn't exist.

At this stage, you might think that the compiler should issue a warning/error about foo on the grounds that it calls a pure virtual function. But instead you should consider the derived non-abstract class where pure has been given an implementation. If you call foo on that class after construction (and assuming you haven't overriden foo), then you will get well-defined behaviour. So again, there is no basis for a warning about foo. foo is well-defined as long as it isn't called in the constructor of Abstract.

Therefore, each method (the constructor and foo) are each relatively OK if you look on them on their own. The only reason we know there is a problem is because we can see the big picture. A very smart compiler would put each particular implementation/non-implementation into one of three categories:

  • Fully-defined: It, and all the methods it calls are fully-defined at every level in the object hierarchy
  • Defined-after-construction. A function like foo that has an implementation but which might backfire depending on the status of the methods it calls.
  • Pure virtual.

It's a lot of work to expect a compiler and linker to track all this, and hence the standard allows compilers to compile it cleanly but give undefined behaviour.

(I haven't mentioned the fact that it is possible to give implementations to pure-virtual methods. This is new to me. Is it defined properly, or is it just a compiler-specific extension? void Abstract :: pure() { })

So, it's not merely undefined 'because the standard says so`. You have to ask yourself 'what behaviour would you define for the above code?'. The only sensible answer is either to leave it undefined or to mandate a run-time error. The compiler and linker won't find it easy to analyse all these dependencies.

And to make matters worse, consider pointers-to-member-functions! The compiler or linker can't really tell if the 'problematic' methods will ever be called - it might depend on a whole load of other things that happen at runtime. If the compiler sees (this->*mem_fun)() in the constructor, it can't be expected to know how well-defined mem_fun is.

Dinse answered 8/2, 2012 at 14:18 Comment(0)
C
2

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

Condor answered 8/2, 2012 at 0:7 Comment(22)
The pure-virtual function in this case has to be definedBaird
-1: But the function in Base is pure virtual and there is nothing to call. No, pure virtual member functions may have implementations. Realistically, if what you said were true, you'd be looking at a compilation error scenario, not UB.Venue
No, a pure virtual function does not have to be defined, which is why an attempt to call it gives you undefined behaviour. In reality it's a function pointer in the v-table (of which the base class implementation may be "null" which probably describes the =0 syntax. If it is given an implementation some compilers may choose to call it but the standard says it's undefined so don't rely on it.Condor
@CashCow: It may be defined. I don't really see why it makes any difference whether the dynamic type of the object is the base type or the derived type. Why should this be UB, and a normal call to an implemented pure virtual function not?Venue
The difference with a non-pure virtual function is that the base class version must exist. Incidentally that is the implementation that gets called, but it is fully defined by the standard, a compiler MUST call it.Condor
@CashCow: That makes no sense at all. Why is that different for a pure? Any function you call "must exist".Venue
funny how someone with 49.2K rep seems clueless about C++. Someone who got huge rep for answering their own question about a weird syntax too. Are you the one who is downvoting answers here?Condor
@CashCow: I added quotes to the standard to back up Lightness and user. Why can't we call pure virtual functions that are defined? In fact, if the destructor is pure virtual, it is required to be defined.Eames
+1. This answer looks OK to me. There has been a huge amount of quite useless commentary on this question and on this answer, and I haven't learned anything from it.Dinse
@LightnessRacesinOrbit: Wouldn't it be linker failure instead? There's no information in the class definition that would tell the compiler whether the function has a body in another compilation unit.Roughdry
A question, guys. A virtual function, in a given class, is either pure or not. Is it true that the initial class definition must state whether it's pure =0; or not? Is it possible for that information to be recorded elsewhere, even in another translation unit? If the implementation can be specified elsewhere, can its 'pureness' be specified elsewhere?Dinse
@LightnessRacesinOrbit, returning to one of your earlier comments "Realistically, if what you said were true, you'd be looking at a compilation error scenario, not UB". There is no guarantee that the compiler can issue a suitable error or warning. Consider this code. There, the constructor calls the non-pure virtual foo. And foo call the pure virtual pure. These two things are fine on their own, but together they will compile cleanly but be meaningless - hence undefined.Dinse
@AaronMcDaid: Someone who doesn't know the topic -- hence asking the original question -- is not qualified to state that this commentary is "useless".Venue
@AaronMcDaid: The linker would be able to produce a diagnostic, just like it does when you call any function without a definition. I still see no reason whatsoever why this should be different for a pure.Venue
@LightnessRacesInOrbit I have given a coded example now of why a link error is probably not appropriate.Condor
@CashCow: It still doesn't explain why the behaviour is different for a pure than for a non-pure. A non-pure virtual member function can be in a "totally different library", too.Venue
@CashCow: Oh, but I suppose for a non-pure virtual member function the definition is required before an executable can be formed...Venue
I think this (open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#230) answers my question.Baird
@LightnessRacesinOrbit, look again at the code I posted. It's an interesting scenario in which the compiler can't (easily) produce a suitable diagnostic. The foo function is perfectly sensible, even though it calls a pure virtual function; remember that, in a derived class, pure might have an implementation and then foo() will call that implementation. Therefore, it would not be appropriate for the compiler to warn about foo. Similarly, the constructor is pretty sensible; it calls foo, and foo is a non-pure virtual function in Abstract.Dinse
@Lightness Races in Orbit. A non-pure virtual function is really a virtual function and goes through the v-table mechanism. The definition of a pure virtual function is not really virtual, it's an illusion, it carries the name of the virtual function but is invoked only ever explicitly and is really protected non-virtual.Condor
@AaronMcDaid: The compiler never produces a diagnostic when a function definition is not found. Ever. It never could.Venue
Please use one of the chat rooms for extended discussion.Midvictorian
D
1

Before discussing why it's undefined, let's first clarify what the question is about.

#include<iostream>
using namespace std;

struct Abstract {
        virtual void pure() = 0;
        virtual void impure() { cout << " Abstract :: impure() " << endl; }
        Abstract() {
                impure();
                // pure(); // would be undefined
        }
        ~Abstract() {
                impure();
                // pure(); // would be undefined
        }
};
struct X : public Abstract {
        virtual void pure() { cout << " X :: pure() " << endl; }
        virtual void impure() { cout << " X :: impure() " << endl; }
};
int main() {
        X x;
        x.pure();
        x.impure();
}

The output of this is:

Abstract :: impure()  // called while x is being constructed
X :: pure()           // x.pure();
X :: impure()         // x.impure();
Abstract :: impure()  // called while x is being destructed.

The second and third lines are easy to understand; the methods were originally defined in Abstract, but the overrides in X take over. This result would have been the same even if x had been a reference or pointer of Abstract type instead of X type.

But this interesting thing is what happens inside the constructor and destructor of X. The call to impure() in the constructor calls Abstract::impure(), not X::impure(), even though the object being constructed is of type X. The same happens in the destructor.

When an object of type X is being constructed, the first thing that is constructed is merely an Abstract object and, crucially, it is ignorant of the fact that it will ultimately be an X object. The same process happens in reverse for the destruction.

Now, assuming you understand that, it is clear why the behaviour must be undefined. There is no method Abstract :: pure which could be called by the constructor or destructor, and hence it wouldn't be meaningful to try to define this behaviour (except possibly as a compilation error.)

Update: I've just discovered that is possible to give an implementation, in the virtual class, of a pure virtual method. The question is: Is this meaningful?

struct Abstract {
    virtual void pure() = 0;
};
void Abstract :: pure() { cout << "How can I be called?!" << endl; }

There will never be an object whose dynamic type is Abstract, hence you'll never be able to execute this code with a normal call to abs.pure(); or anything like that. So, what is the point of allowing such a definition?

See this demo. The compiler gives warnings, but now the Abstract::pure() method is callable from the constructor. This is the only route by which Abstract::pure() can be called.

But, this is technically undefined. Another compiler is entitled to ignore the implementation of Abstract::pure, or even to do other crazy things. I'm not aware of why this isn't defined - but I wrote this up to try to help clear up the question.

Dinse answered 8/2, 2012 at 0:3 Comment(5)
Let's suppose you define Abstract::pure() { cout << "Abstract pure" << endl; }, maintaining the declaration virtual void pure() = 0;. What would be the difference, as compared to the impure() virtual function ?Baird
@user1042389, I wasn't aware of that possibility until a few minutes ago. I have updated this answer as a result. I used to think virtual method were either pure or not - this third state (pure defined) seems pretty weird to me! I don't really know what to say.Dinse
You can call Abstract::pure() from Abstract or any derived class that has access to it by invoking it just like this, with the fully qualified name.Condor
I think this (open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#230) answers my questionBaird
The standard issue was closed and moved here: open-std.org/jtc1/sc22/wg21/docs/cwg_closed.html#230Elburr

© 2022 - 2024 — McMap. All rights reserved.