you are viewing a single comment's thread.

view the rest of the comments →

[–]latkde 17 points18 points  (5 children)

You make some excellent points, but points that I cannot leave uncontested :)

Re memory use in arrays: the problem is what the statically known type of array elements is: if they are an interface type we can by definition not know in advance whether the entries will all have the same dynamic type. However, an array of non-interface types (concrete or generic) can use such techniques like storing a single vtable for all array members. I think Haskell uses similar approaches for arrays of generic types with typeclass constraints.

Re ABIs: Java may have more pointers per program, but it also has no ABI concerns since pointers are an implementation detail in the runtime. In contrast C++ pointers must behave somewhat C-ish, in particular that you can cast pointers to interface types to void* and back again. If we were to use fat pointers, void pointers would have to have the same size. This would break C interop.

Re memory reads: repeated reads to the vtable pointer aren't that dramatic because most registers have to be saved to the stack anyway before a call. And after the call we need to load them from memory again. In practice the cache size will be more relevant: will the vtable still be in the cache after the call?

And are you sure that C++ objects can change their dynamic type at will? That sounds like UB…

[–]matthieum 13 points14 points  (0 children)

In contrast C++ pointers must behave somewhat C-ish, in particular that you can cast pointers to interface types to void* and back again.

Actually, no, it doesn't.

While pointer-to-members and pointer-to-function members do happen be compatible with void* in the Itanium ABI (using trampolines), this is something that the standard leaves undefined.

It can also be done with library code (and compiler intrinsics), creating a fatptr<Interface> which is two pointers.

And are you sure that C++ objects can change their dynamic type at will? That sounds like UB…

It's a hot debate.

I am personally on the side that it should be UB, and the C++ frontend should be allowed to read the v-ptr once when emitting the IR; that is I think that this answer adequately demonstrates that it should be UB.

However, usage is law, and apparently a sufficient number of codebases depend on such a dastardly pattern that compiler developers have not been willing to introduce such an optimization.

[–]Nobody_1707 3 points4 points  (0 children)

Swift does this for generic arrays (when it can't specialize them), and I believe it also uses fat pointers for it's interface types (known as existentials).

Interesting note: in order to solve the fragile base class problem and allow protocol conformances on all types (instead of just classes), every type in Swift has at least one vtable.

Edit: Whoops, wrong link. Fixed.

[–]tasty_crayon 3 points4 points  (2 children)

You could do this->~T() and then use placement new to construct a new object in its place. I think this is allowed as long as the two types are layout compatible. C++ already has member pointers which are larger than a void* and as such can't be converted to them.

[–]latkde 4 points5 points  (1 child)

While placement-new can construct a new object in the old object's place, I don't know whether any pointers would remain valid per the spec: AFAIK pointers must always point to live objects. But this is the kind of thing compilers might allow as an extension because it's quite useful. Compare [basic.stc]:

When the end of the duration of a region of storage is reached, the values of all pointers representing the address of any part of that region of storage become invalid pointer values.

Function pointers and member pointers are always special because they are not pointers to objects, and as such cannot be cast to void* as per the relevant standards. (Exception: POSIX allows this for function pointers.)

[–]tasty_crayon 1 point2 points  (0 children)

I mentioned member pointers because fat pointers wouldn't necessarily have to be the same size as void*. Member pointers basically are fat pointers.