Problems with C++ abstract class (I can do it in Java, but not in C++!)
Asked Answered
E

6

5

First of all, I searched this problem and found a lot of similiar questions, but I couldn't find an answer that fixed my problem. I am very sorry if that is just me being dumb.

What I am trying to do is make the constructor of an abstract class call a function that is pure virtual. In Java, this works, because the subclass provides the implementation of the abstract method that is called. However, in C++, I get this linker error:

test.o:test.cpp:(.text$_ZN15MyAbstractClassC2Ev[MyAbstractClass::MyAbstractClass
()]+0x16): undefined reference to `MyAbstractClass::initialize()'
collect2: ld returned 1 exit status

Here is my code:

#include <iostream>

class MyAbstractClass {
protected:
    virtual void initialize() = 0;

public:
    MyAbstractClass() {
        initialize();
    }
};

class MyClass : public MyAbstractClass {
private:
    void initialize() {
        std::cout << "yey!" << std::endl;
    }
};

int main() {
    MyClass *my = new MyClass();
    return 0;
}

As a further explanation of what I am trying to do, here is code in Java that achieves my goal:

public abstract class MyAbstractClass {

    public MyAbstractClass() {
        initialize();
    }

    protected abstract void initialize();
}

public class MyClass extends MyAbstractClass {

    protected void initialize() {
        System.out.println("Yey!");
    }

    public static void main(String[] args) {
        MyClass myClass = new MyClass();
    }
}

This code prints "Yey!". Any help much appreciated!

Ecru answered 27/2, 2012 at 23:12 Comment(3)
Never call virtual functions in constructors or destructors. And why do you need an initialise function anyway? That's what constructors are for.Sodium
parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.5 will do better at explaining why what you're trying to do won't work than I will be able to.Spirillum
Even in Java this is discouraged. Calling a method in a derived class before that class is initialized is troublesome as the members variables have yet to be initialized by the constructor. So all you can do safely is print a message anyway. Look at the PIMPL pattern for a solution to your problem.Audiometer
B
4
MyAbstractClass() {
    initialize();
}

That will not perform a virtual dispatch to MyClass::initialize(), because at this stage of the object's construction its MyClass parts haven't been created yet. Thus you really are invoking MyAbstractClass::initialize() and, as such, it must be defined. (Yes, pure virtual member functions can be defined.)

Try to avoid invoking virtual member functions from constructors, because this sort of stuff will happen and catch you out. It rarely makes sense to do it.

Also, try to avoid initialize() functions; you already have constructors to play with.


Update

Actually, though you may take the above as read for any other virtual member function, invoking a pure virtual member function from the constructor yields Undefined Behaviour. So don't even try!

Benefaction answered 27/2, 2012 at 23:25 Comment(10)
+1 for the bit about avoiding pointless initialise functionsSodium
It won't perform a virtual dispatch because of optimization, not because the virtual machinery isn't set up. For example, you could call a member function (of the base class) defined in another compilation unit, which calls a virtual function. That call would absolutely be made virtually (and find the most-overridden version considering only the type whose constructor is currently running and base classes thereof).Kelvinkelwen
Even if the pure virtual function were defined, you'd still get undefined behaviour calling it virtually from the constructor.Endear
@BenVoigt: I never said the virtual machinery isn't set up. And optimisation has nothing to do with it.Benefaction
@SethCarnegie, initialize functions aren't pointless if you have multiple forms of constructors that have operations in common. DRY principle and all. Or were you only speaking of this specific example?Haskel
@MikeSeymour: Oh, indeed, I believe it's UB when the virtual function is pure.Benefaction
@Mark: Then the derived class constructor can call the initialize function.Kelvinkelwen
@LightnessRacesinOrbit: Before you edited your answer, it said something along the lines of "this call won't be virtually dispatched". That's what I was commenting on.Kelvinkelwen
@MarkRansom: They tend to break RAII, though if you're careful with access levels then you can avoid that. But for language newcomers I prefer a catch-all "don't" recommendation.Benefaction
@BenVoigt: No, it never did. I said -- and I still do -- that "this call won't result in a virtual dispatch to MyClass::initialize()", which is absolutely correct. I'd originally left off the ::initialize(), but that doesn't make the statement incorrect. Instead you get a virtual dispatch to [a function in] MyAbstractClass ... or, you would if the pureness didn't render the entire thing UBBenefaction
E
2

In C++, you can't call a pure virtual function from a constructor or destructor (even if it has a definition). If you call a non-pure one, then it will be dispatched as if the object's type were the class under construction, so you'll never be able to call a function defined in a derived class.

In this case, you don't need to; the derived class's constructor will be called after the base class's, so you get the desired result from:

#include <iostream>

class MyAbstractClass {
public:
    MyAbstractClass() {
        // don't do anything special to initialise the derived class
    }
};

class MyClass : public MyAbstractClass {
public:
    MyClass() {
        std::cout << "yey!" << std::endl;
    }
};

int main() {
    MyClass my;
    return 0;
}

Note that I also changed my to an automatic variable; you should get in the habit of using them whenever you don't need dynamic allocation, and learn how to use RAII to manage dynamic resources when you really do need them.

Endear answered 27/2, 2012 at 23:27 Comment(2)
pure virtual functions can have definitions, but I'm not sure if such a definition can be found this wayKelvinkelwen
@BenVoigt: Even if it had a definition, it would still give undefined behaviour to call it virtually from the constructor. A pure virtual function can only be called statically.Endear
S
1

Let me quote Scott Meyers here (see Never Call Virtual Functions during Construction or Destruction):

Item 9: Never call virtual functions during construction or destruction.

I'll begin with the recap: you shouldn't call virtual functions during construction or destruction, because the calls won't do what you think, and if they did, you'd still be unhappy. If you're a recovering Java or C# programmer, pay close attention to this Item, because this is a place where those languages zig, while C++ zags.

The issue: during object construction the virtual function table might not yet be ready. Just imagine that your class is eg. fourth in line of inheritance. Constructors are called in inheritance order, so while calling this pure virtual (or even if it was non-pure) you would like the base class to call initialize for an object that is not yet complete!

Scheck answered 27/2, 2012 at 23:22 Comment(1)
That's [good] advice, but not a technical restriction. In fact the dynamic type of the object is well-defined inside this constructor for any given object initialisation; it just may not be (and, in this case, isn't) the eventual most-derived type of the object.Benefaction
C
1

The C++ side has been handled in other answers, but I want to add a remark on the Java side of it. Calling a virtual function from a constructor is a problem in all cases, not just in C++. Basically what the code is trying to do is execute a method on an object that has not yet been created and that is an error.

The two solutions implemented in the different languages differ in trying to make some sense of what your code is trying to do. In C++ the decision is that during construction of a base object, and until construction of the derived object starts, the actual type of the object is base, which means that there won't be dynamic dispatch. That is, the type of the object at any time is that of the constructor being executed[*]. While this is surprising to some (you among others), it provides a sensible solution to the problem.

[*] Conversely the destructor. The type also changes as the most derived constructors complete.

The alternative in Java is that the object is of the final type from the beginning, even before construction has completed. In Java, as you demonstrated, the call will be dispatched to the final overrider (I am using C++ slang here: to the last implementation of the virtual function in the execution chain), and that can cause unwanted behavior. Consider for example this implementation of initialize():

public class MyClass extends MyAbstractClass {
   final int k1 = 1;
   final int k2;
   MyClass() {
      k2 = 2;
   }
   void initialize() {
      System.out.println( "Constant 1 is " + k1 + " and constant 2 is " + k2 );
   }
}

What is the output of the previous program? (Answer at the bottom)

More than just a toy example, consider that MyClass provides some invariants that are set at construction time and hold for the whole lifetime of the object. Maybe it holds a reference to a logger on which data can be dumped. By looking at the class, you can see that the logger is set in the constructor and assume that it cannot be reset anywhere in the code:

public class MyClass extends MyAbstractClass {
   Logger logger;
   MyClass() {
      logger = new Logger( System.out );
   }
   void initialize() {
      logger.debug( "Starting initialization" );
   }
}

You probably see now where this is going. By looking at the implementation of MyClass there does not seem to be anything wrong at all. logger is set in the constructor, so it can be used in all methods of the class. Now the problem is that if MyAbstractClass calls on a virtual function that gets dispatched then the application will crash with a NullPointerException.

By now I hope that you understand and value the C++ decision of not performing dynamic dispatch and thus avoid executing functions on objects that have not yet been fully initialized (or conversely have already been destroyed, if the virtual call is in the destructor).

(Answer: this might depend on the compiler/JVM, but when I tried this long long time ago, the line printed Constant 1 is 1 and constant 2 is 0. If you are happy with that, fine by me, but I found that to be surprising... The reason for the 1/0 in that compiler is that the process of initialization first sets the values that are in the variable definition, and then calls the constructors. This means that the first step of construction would set k1 before calling MyAbstractBase constructor, that would call initialize() before MyBase constructor has run and set the value of the second constant).

Cutch answered 27/2, 2012 at 23:45 Comment(0)
K
0

During construction and destruction, the virtual table is set up appropriately for the base subobject being constructed or destroyed. This is the theoretically correct thing to do, because the more-derived class is not alive (its lifetime either hasn't yet started or has already ended).

Kelvinkelwen answered 27/2, 2012 at 23:28 Comment(0)
S
-1

As already explained by @Seth, you cannot call virtual functions in a constructor. More specifically, the virtual dispatch mechanism is disabled during construction and destruction. Either make your initialize member function nonvirtual and implement it in the base class, or have the user call it explicitly.

Stringhalt answered 27/2, 2012 at 23:17 Comment(3)
I would say it is not disabled but it maynot do what it is intended to do.Kiel
You can call non-pure virtual functions from a constructor; they'll just be dispatched as if the class under construction were the final overrider.Endear
@BenVoigt: And I already noted that my "ninja edit" did not change the semantics of my answer in any way. The only "ninja edit" was s/MyClass/MyClass::initialise()/. "There is no virtual call to MyClass" is just as accurate, if somewhat less precise for omitting the name of the function that one might otherwise expect to be invoked.Benefaction

© 2022 - 2024 — McMap. All rights reserved.