On Friday, 9 August 2019 at 12:26:59 UTC, John Colvin wrote:
import std.stdio;

interface I
{
    void foo();
}

class C : I
{
    override void foo() { writeln("hi"); }
}

abstract class AC
{
    void foo();
}

class D : AC
{
    override void foo() { writeln("hi"); }
}

void main()
{
    auto c = new C();
    writeln(0);
    (cast(I)cast(void*)c).foo();
    writeln(1);
    (cast(C)cast(void*)c).foo();
    writeln(2);
    (cast(I)cast(C)cast(void*)c).foo();

    auto d = new D();
    writeln(3);
    (cast(AC)cast(void*)d).foo();
    writeln(4);
    (cast(D)cast(void*)d).foo();
    writeln(5);
    (cast(AC)cast(D)cast(void*)d).foo();
}

This produces the output:

0
1
hi
2
hi
3
hi
4
hi
5
hi

Why is there no "hi" between 0 and 1?

We're getting into somewhat advanced topics now. This is described in the Application Binary Interface page of the documentation[0]. In short: classes and interfaces both use a vtable[1] that holds pointers to each of their methods. When we cast a class instance to an interface, the pointer is adjusted, such that the interface's vtable is the first member. Casting via `void*` bypasses this adjustment.

Using `__traits(classInstanceSize)`, we can see that `C` has a size of 12 bytes, while `D` only is 8 bytes (24 and 16 on 64-bit). This corresponds to the extra interface vtable as described above.

When we first cast to `void*`, no adjustment happens, because we're not casting to an interface. When we later cast the `void*` to an interface, again no adjustment happens - in this case because the compiler doesn't know what we're casting from.

If we use `__traits(allMembers, C)`, we can figure out which methods it actually has, and implement those with some extra debug facilities (printf):

class C : I
{
    override void foo() { writeln("hi"); }
override string toString() { writeln("toString"); return ""; } override hash_t toHash() { debug printf("toHash"); return 0; } override int opCmp(Object o) { writeln("opCmp"); return 0; } override bool opEquals(Object o) { writeln("opEquals"); return false; }
}

If we substitute the above in your program, we see that the `toString` method is the one being called. This is simply because it's at the same location in the vtable as `foo` is in `I`'s vtable.

When casting from a class to a superclass, no pointer adjustment is needed, as the vtable location is the same for both.

We can look closer at the vtable, and see that for a new subclass, additional entries are simply appended at the end:

class C {
    void foo() {}
}

class D : C {
    void bar() {}
}

unittest {
    import std.stdio;

    C c = new C();
    D d = new D();

    writeln("Pointer to foo(): ", (&c.foo).funcptr);
    writeln("Pointer to bar(): ", (&d.bar).funcptr);

    writeln("Pointer to foo() in C's vtable: ", c.__vptr[5]);

    writeln("Pointer to foo() in D's vtable: ", d.__vptr[5]);
    writeln("Pointer to bar() in D's vtable: ", d.__vptr[6]);
}

As we see, `foo()` has the position in the vtable for both `c` and `d`, while `D`'s new `bar()` method is added as the next entry.

--
  Simen

[0]: https://dlang.org/spec/abi.html
[1]: https://en.wikipedia.org/wiki/Virtual_method_table

Reply via email to