D casting broke?
Joerg Joergonson via Digitalmars-d-learn
digitalmars-d-learn at puremagic.com
Tue Jun 21 08:35:08 PDT 2016
On Tuesday, 21 June 2016 at 02:39:25 UTC, H. S. Teoh wrote:
> On Tue, Jun 21, 2016 at 02:09:50AM +0000, Joerg Joergonson via
> Digitalmars-d-learn wrote: [...]
>> Lets suppose A -> B means B is derived from A. That is, any
>> object of B can be cast to A because the memory layout of A is
>> contained in B and any object of B can be accessed as if it
>> were an A.
>
> Correct.
>
>
>> Template parameters also can have this property since they are
>> types.
> [...]
>
> The template *parameters* can also have this property. But the
> *template* itself may not. Contrived example:
>
> class A { int x; }
> class B : A { int y; }
>
> struct Templ(C : class) {
> int[C.sizeof] data;
> }
>
> Templ!A a;
> Templ!B b;
>
> a = b; // should this be allowed?
>
> It should be clear that allowing this would cause problems,
> because in spite of the relationship between A and B, and hence
> the relationship between the template arguments of a and b, the
> same relationship does not hold between Templ!A and Templ!B
> (note that .data is an array of ints, not ubytes, and may not
> contain data in any layout that has any corresponds with the
> relationship between A and B).
>
> Another contrived example:
>
> class A { int x; }
> class B : A { int y; }
>
> struct Templ(C : class) {
> static if (C.sizeof > 4) {
> string x;
> } else {
> float y;
> }
> }
>
> Allowing implicit casting from Templ!B to Templ!A would not
> make sense, because even though the respective template
> arguments have an inheritance relationship, the Templ
> instantiation made from these classes has a completely
> unrelated and incompatible implementation.
>
Well, I never mentioned any implicit casting. Obviously explicit
casting wouldn't make sense either. It is a good example as it
can show that Templ!A is completely different than Templ!B and no
conversion is every possible even if A and B are related.
But I still think these are different examples.
You are talking about A!a vs A!b while I am talking about A!a vs
B!b. I believe, but could be mistaken, that there is a subtle
difference. I know it seems like B!b can be reduced to A!b, and
the type system allows this... but if it never happens then all
these cases explaining the problem of going from A!b to A!a are
moot.
> Now granted, these are contrived examples, and in real-life we
> may not have any real application that requires such strange
> code. However, the language itself allows such constructs, and
> therefore the compiler cannot blindly assume any relationship
> between Templ!A and Templ!B even though there is a clear
> relationship between A and B themselves.
I agree. I am not asking for blind assumptions. When I inform the
compiler I want to cast to an object that I know should
succeed(it was designed to do so) I don't expect a null. (As has
been mentioned, I can do this using the void* trick, so there is
a way)
> What should be done if we wish to allow converting Templ!B to
> Templ!A, though? One way (though this still does not allow
> implicit casting) is to use opCast:
>
> struct Templ(C : class) {
> ... // implementation here
>
> auto opCast(D : class)
> if (is(C : D)) // C must be a base class of D
> {
> ...
> // do something here to make the conversion
> // valid. Maybe something as simple as:
> return cast(Templ!D) this;
>
> // (provided that there are no memory layout
> // problems in Templ's implementation, of
> // course).
> }
> }
>
> Implementing this using opCast actually gives us an even more
> powerful tool: provided it is actually possible to convert
> between potentially incompatible binary layouts of Templ!A and
> Templ!B, the opCast method can be written in such a way as to
> construct Templ!A from Templ!B in a consistent way, e.g., by
> treating B as a subclass of A and calling the ctor of Templ!A,
> or, in the case of my contrived examples, do something
> non-trivial with the .data member so that the returned Templ!A
> makes sense for whatever purpose it's designed for. It allows
> the implementor of the template to specify exactly how to
> convert between the two types when the compiler can't possibly
> divine this on its own.
>
While this is nice, the problem was how to convert. Even in
opCast I would get a null and I wouldn't want to reconstruct A!a
from B!b because that would essentially entail duplication. Of
course, now I know I can just cast to void* then back and
essentially trick/bypass the compilers type system checking.
> This is kind of bringing a nuclear warhead to an anthill,
> though. In my own code where I have a template wrapping around
> types that need to convert to a common base type, I find it
> more useful to use the following pattern instead:
>
> class A { ... }
> class B : A { ... }
>
> class WrapperBase {
> ... // common stuff for all instantiations of Wrapper!X
> }
>
> class Wrapper(C : class) : WrapperBase {
> ... // stuff specific to C
> }
>
> Wrapper!A a;
> Wrapper!B b;
> WrapperBase wb = a; // OK
> wb = b; // also OK
>
> This may or may not be what you're looking for, though.
>
>
This is exactly what I have done! ;) Just a bit more.
Essentially I have
class DoubleWrapper(D : C) : Wrapper!C
and the problem is that Wrapper contains overloads from
WrapperBase that cast to Wrapper and when these overloads are
called in DoubleWrapper(because I didn't overload anything in
DoubleWrapper) the casts return null. (ecause DoubleWrapper!D
cannot convert o Wrapper!C.
Basically I have to down cast something in WrapperBase to use it
in Wrapper, but I know it is of type Wrapper when I am "in"
Wrapper. I also know it is of type DoubleWrapper when in
DoubleWrapper. The cast sort of needs to be "polymorphic"... but,
of course casts are static so this doesn't work.
In all the counter examples give, it's as if I had
class DoubleWrapper(D : C); instead of
class DoubleWrapper(D : C) : Wrapper!C
Or no DoubleWrapper at all. Again, I think there is some
difference and some information provided by the inheritance that
makes things different. It seems like a complete hack to have to
cast to void* and back to make things work when there is a way.
For example, Your Templ class is very different, but
DoubleWrapper can't do this and be *completely* different because
it inherits Wrapper;
class DoubleWrapper(D : C)
{
... does something strange depending on different D...
}
Different than
class DoubleWrapper(D : C) : Wrapper!C
{
... does something crazy depending on different D...
... But cannot change the fact that it has a compatible memory
footprint of Wrapper!C(since it inherits from it)
}
To take your example:
> class A { int x; }
> class B : A { int y; }
>
class X(T : A);
> struct Templ(T : (B : A)) : X!A {
> static if (C.sizeof > 4) {
> string x;
> } else {
> float y;
> }
// + There's still a complete "copy" of X!C lurking
in here
> }
Different right? The X!C makes Templ!C different, regardless of
the crazy stuff its does based on C, it still must be compatible
with X!C because that's what inheritance tells us. We don't get
that in your original example because there is no inheritance
going on(well, excluding A and B)
In your original, Templ!A and templ!B may not be relatable, but
in mine X!A and Templ!B are.
Now, I suppose you might be able to prove this wrong, say by
constructing something funky in X:
class X(T : A)
{
static if (T.sizeof > 4)
int[] x;
else
string y;
}
But regardless, X!A is fixed w.r.t to Templ!B. So even though it
is "crazy" depending on the template parameter, the craziness
doesn't get though.
If we had
class Templ(T : B) : X!T
then it can be crazy just as Templ can be, but this is different.
This has very little to do with A.
Basically there are constraints in the design that prevent
craziness from taking place(I think). My Button!ButtonItem and
Slider!SliderItem could have easily been coded as
ButtonButtonItem and SliderSliderItem : ButtonButtonItem(although
with increased obfuscation and reduced extensibility).
My point is, unless someone can prove me wrong, is in the actual
problem I am presenting, not in the over simplified cases that
exclude my case(In which case one would have to prove why the
simplification holds rather than assuming and proving why the
simplification is false), resorting to a void* cast is a hack and
not necessary.
It should be quite logical for the compiler, possibly with a
little help, to realize the cast works. e.g.,
Parallel_Cast(Button!ButtonItem)Slider!SliderItem.
(remember, Slider!(X : SliderItem) inherits from
Button!ButtonItem directly! In fact, I could simply alias
Button!ButtonItem to Q and then it inherits from Q... and so must
always be castable to Q, right?)
e.g.,
class Button(T : ButtonItem);
alias Q = Button!ButtonItem;
class Slider(T : (SliderItem : ButtonItem)) : Q;
cast(Q)Slider!X; // Should always work or no?
This shows in a pretty obvious way that Button!ButtonItem is
static and hence is just regular inheritance.
Now, my case is slightly more complex:
class Slider(T : (SliderItem : ButtonItem)) : (Z!T : Q);
Z!T is a more general Button!ButtonItem, but it is still, at its
base, a Q(hence the notation)... so always casting to Q should
still work.
Q is a sort of "fixed point" and no matter what we build on top
of it, no matter how complex, we should be able to cast down to
it. Just like I can cast to Widget or WrapperBase because they
are "fixed points"(and the kinda the reason why they are useful)
Again, it's possible I'm missing something or just being
ignorant, but I don't think anyone has truly shown this(contrived
examples are ok as long as they are analogous to the real problem
and not oversimplified).
It would be nice to know, though, if this is just a short coming
of D or if there a flaw in my logic. At least I could live with
D's flaws ;)
More information about the Digitalmars-d-learn
mailing list