Search code examples
c++clangvirtual-functions

Call to virtual method during construction bypasses virtual dispatch C++


I have the following code:

class IInterface {
  public:
    virtual ~IInterface() {}
    virtual void doSomething() = 0;
};

class Concrete : public IInterface {
  public:
    Concrete() : data{0} {doSomething();}
    void doSomething() override {std::cout << "I am doing something concrete" << std::endl;}
  private:
    int data;
};

When I compile, Clang gives the following output:

Call to virtual method doSomething during construction bypasses virtual dispatch

  • What does the message mean? Is it warning me that static dispatch is done instead of virtual dispatch, or is it a warning of UB?

Note that if in the Concrete's constructor I do Concrete::doSomething(), the message goes away.

  • Is this UB?

Say we do Concrete c;. This is what I think it happens:

  1. Memory for object c is allocated.
  2. The abstract class's constructor is called. The __vptr is pointing to a pure virtual function.
  3. The Concrete constructor is called. Initialization of any non-static members take place. Initialization complete, the __vptr now points to Concrete's implementation of doSomething(). The constructor body is ran and it should call the overridden doSomething().

Is anything else than what I describe happening?


EDIT

It is clang-tidy that gives the message LLVM version 17.0.6


Solution

  • There's nothing inherently wrong with calling virtual functions from constructors or destructors, and doing so does not bypass virtual dispatch. People who say that are oversimplifying what's really going on.

    struct base {
        virtual void f() { }
        void g() { f(); }
    
        base() {
            f(); // always calls base::f
            g(); // always calls base::f
        }
    };
    
    struct intermediate : base {
        void f();
        intermediate() {
            f(); // always calls intermediate::f
            g(); // always calls intermediate::f via virtual dispatch
        }
    };
    

    The true rule is simply that virtual function calls in constructors (and destructors) are dispatched according to the rules for the class whose constructor is currently running.

    intermediate i;
        // base::base calls to f() and g() both call base::f
        // intermediate::intermediate calls to f() and g() both call intermediate::f
    
    i.g();          // calls intermediate::f
    

    People tend to get confused when there's another level of construction:

    struct derived : intermediate {
        derived() : intermediate() { }
        void f() { }
    };
    
    derived d; // base::base() calls to f() and g() both call base::f
               // intermediate::intermediate() calls to f() and g() both call intermediate::f
    
    d.f(); // calls derived::f
    d.g(); // cals derived::f
    

    The key here is that during the construction of the intermediate subobject all virtual calls act as if they were being called on an object of type intermediate, even though the type of the actual object being constructed is derived. There's no derived in the constructor for intermediate.

    None of the calls to f() "bypass" virtual dispatch. The virtual function calls go to the version of f() that's defined for the class whose constructor is running. Folks who characterize this as "bypassing" virtual dispatch are begin lazy. Folks who claim that calling virtual functions from constructors and destructors is inherently dangerous are being alarmist. Yes, you have to understand what's going on. No, there are no demons lurking here.