Thread overview
Virtual method call from constructor
Apr 04, 2023
Chris Katko
Apr 04, 2023
Ali Çehreli
Apr 05, 2023
Johan
April 04, 2023

dscanner reports this as a warning:

struct foo{
this()
  {
  /* some initial setup */
  refresh();
  }
void refresh() { /* setup some more stuff */}
// [warn] a virtual call inside a constructor may lead to unexpected results in the derived classes

}

Firstly, are all calls virtual in a struct/class in D? Is this any different from C++? IIRC, in C++, a struct cannot have virtual calls, and virtual methods are explicit keywords in classes.

Second, could you give me some case examples where this problem occurs? Is the issue if I override refresh in a derived class, and the base class will accidentally use child.refresh()?

Third, is there the expectation that you should never call any internal, private, methods inside a constructor? Or do I just call/structure it a different way?

For a concrete example: I have a particle struct. It makes sense to me, to have initial setup code (placed in the refresh() function) called by this(), that later I can then call again; to reset the struct to an initial state in-memory without re-allocations.

I imagine in D that there's probably something like:

particles[235] = foo.init;

but that blows up in a scenario where I'm only partially resetting the struct data. For example, if I had a bunch of pointers to system modules, those values don't need to be nulled and re-set every time in this(), whereas the physical data like position, velocity, angle, need reset in refresh(). You could architect around that, but I'm trying to learn the language mechanics.

April 04, 2023
> Firstly, are all calls virtual in a struct/class in D?

In D structs are always value types without a vtable or inheritance.

Classes are always reference types with a vtable, but some of the methods may be final and hence not in vtable.

> Second, could you give me some case examples where this problem occurs? Is the issue if I override refresh in a derived class, and the base class will accidentally use child.refresh()?

Yes. It'll call the entry in the vtable, not the one declared in the class if its overridden.

> Third, is there the expectation that you should _never_ call any internal, private, methods inside a constructor? Or do I just call/structure it a different way?

No. Do what you need to do. Dscanner is a linter that may be a bit sensitive just in case you didn't realize that the behavior is doing something that you probably haven't considered, which could be problematic.

April 04, 2023
On 4/4/23 00:08, Chris Katko wrote:
> dscanner reports this as a warning:
>
> ```D
> struct foo{
> this()
>    {
>    /* some initial setup */
>    refresh();
>    }
> void refresh() { /* setup some more stuff */}
> // [warn] a virtual call inside a constructor may lead to unexpected
> results in the derived classes
>
> }
> ```

I can understand that error for a class. Is that really a struct? If so, then it looks like a dscanner bug to me.

> Firstly, are all calls virtual in a struct/class in D?

All functions are by-default virtual only for classes.

To note, not the "call" but the function can be virtual. When calls are involved, there can be static binding and dynamic binding. Static binding is when a call is resolved at compile time. Dynamic binding is resolved at run time through the vtbl pointer.

> Is this any
> different from C++?

Yes. In C++, all functions are non-virtual by-default.

> IIRC, in C++, a struct cannot have virtual calls,

No, structs and classes are functionally exactly the same in C++. The only difference is their default member accessibility: public for structs and private for classes.

> and virtual methods are explicit keywords in classes.

Yes.

> Second, could you give me some case examples where this problem occurs?
> Is the issue if I override refresh in a derived class, and the base
> class will accidentally use child.refresh()?

Yes. :) Here is what I had learned in my C++ days: The vtbl pointer is stamped before the constructor is entered. However, an object is not yet complete without its constructor exiting. The base class's constructor calling a virtual function of its derived part might be disastrous because the derived part is not fully constructed yet. (Well, it is not as disastrous in D because all members have default values by-default.)

import std;

class Base {
    void foo() {
        writeln(__FUNCTION__);
    }

    this(int i) {
        writeln("Entered ", __FUNCTION__);
        foo();                              // <-- THE PROBLEM
        writeln("Exiting ", __FUNCTION__);
    }
}

class Derived : Base {
    override void foo() {
        writeln(__FUNCTION__);
    }

    this(int i) {
        writeln("Entered ", __FUNCTION__);
        super(i);
        writeln("Exiting ", __FUNCTION__);
    }
}

void main() {
    auto d = new Derived(42);
}

Here is the (annotated) output:

Entered deneme.Derived.this
Entered deneme.Base.this
deneme.Derived.foo            <-- NO!
Exiting deneme.Base.this
Exiting deneme.Derived.this

The highlighted line is a call to a derived function but even the base part of the object is not finished its construction yet. The weird thing is Base is initiating the call but it has no idea even whether it's a part of an inheritance hierarchy, what other types are involved, etc.

Ali

April 05, 2023

On Tuesday, 4 April 2023 at 07:08:52 UTC, Chris Katko wrote:

>

dscanner reports this as a warning:

struct foo{
this()
  {
  /* some initial setup */
  refresh();
  }
void refresh() { /* setup some more stuff */}
// [warn] a virtual call inside a constructor may lead to unexpected results in the derived classes

}

Firstly, are all calls virtual in a struct/class in D? Is this any different from C++?

Regarding this warning, the big difference between C++ and D classes is that a D class object is of type Child its whole life including during the Base class constructor execution. In C++ a class object starts as Base, Base::constructor is executed, then type is changed to Child (implemented by overwriting the vtable pointer), then Child::constructor is executed.

-Johan

April 05, 2023

On 4/4/23 3:08 AM, Chris Katko wrote:

>

Second, could you give me some case examples where this problem occurs? Is the issue if I override refresh in a derived class, and the base class will accidentally use child.refresh()?

An example of a problem:

class Base
{
   this() { virtualCall(); }
   void virtualCall() {}
}

class Derived : Base
{
   int *data;
   this() { data = new int; }
   override void virtualCall() { *data = 5; }
}

A derived class constructor without an explicit super() call, is injected with a super() call at the beginning of the constructor.

So in this case, the Base constructor runs before the Derived constructor. The Base ctor calls virtualCall before Derived is ready for it.

To fix this, you can explicitly call super() after initializing the data:

this() {data = new int; super(); }

So there are ways to do this in a reasonable way, but that is why the warning exists.

-Steve