Here is the symmetric case to illustrate upcasting and downcasting.
type A is tagged ...; -- one parent type
type B is tagged ...; -- another parent type
...
type C; -- the new type, to be a mixture of A and B
type AC (Obj : access C'Class) is
new A
with ...;
-- an extension of A to be mixed into C
type BC (Obj : access C'Class) is
new B
with ...;
-- an extension of B to be mixed into C
type C is
tagged limited record
A : AC (C'Access);
B : BC (C'Access);
... -- other stuff if desired
end record;
We can now pass an object of type C to anything that takes an A or B as follows (this presumes that Foobar and QBert are primitives of A and B, respectively, so they are inherited; if not, then an explicit conversion (upcast) to A and B could be used to call the original Foobar and QBert).
XC : C;
...
Foobar (XC.A);
QBert (XC.B);
If we want to override what Foobar does, then we override Foobar on AC. If we want to override what QBert does, then we override QBert on BC.
Note that there are no naming conflicts, since AC and BC are distinct types, so even if A and B have same-named components or operations, we can talk about them and/or override them individually using AC and BC.
Upcasting (from C to A or C to B) is trivial -- A(XC.A) upcasts to A; B(XC.B) upcasts to B.
Downcasting (narrowing) is also straightforward and safe. Presuming XA of type A'Class, and XB of type B'Class:
AC(XA).Obj.all downcasts to C'Class (and verifies XA in AC'Class)
BC(XB).Obj.all downcasts to C'Class (and verifies XB in BC'Class)
You can check before the downcast to avoid a Constraint_Error:
if XA not in AC'Class then -- appropriate complaint
if XB not in BC'Class then -- ditto
The approach is slightly simpler (though less symmetric) if we choose to make A the "primary" parent and B a "secondary" parent:
type A is ...
type B is ...
type C;
type BC (Obj : access C'Class) is
new B
with ...
type C is
new A
with record
B : BC (C'Access);
... -- other stuff if desired
end record;
Now C is a "normal" extension of A, and upcasting from C to A and (checked) downcasting from C'Class to A (or A'Class) is done with simple type conversions. The relationship between C and B is as above in the symmetric approach.
Not surprisingly, using building blocks is more work than using a "builtin" approach for simple cases that happen to match the builtin approach, but having building blocks does ultimately mean more flexibility for the programmer -- there are many other structures that are possible in addition to the two illustrated above, using the access discriminant building block.
For example, for mixins, each mixin "flavor" would have an access discriminant already:
type Window is ... -- The basic "vanilla" window
-- Various mixins
type Win_Mixin_1 (W : access Window'Class) is ...
type Win_Mixin_2 (W : access Window'Class) is ...
type Win_Mixin_3 (W : access Window'Class) is ...
Given the above vanilla window, plus any number of window mixins, one can construct a desired window by including as many mixins as wanted:
type My_Window is
new Window
with record
M1 : Win_Mixin_1 (My_Window'access);
M3 : Win_Mixin_3 (My_Window'access);
M11 : Win_Mixin_1(My_Window'access);
... -- plus additional stuff, as desired.
end record;
As illustrated above, you can incorporate the same "mixin" multiple times, with no naming conflicts. Every mixin can get access to the enclosing object. Operations of individual mixins can be overridden by creating an extension of the mixin first, overriding the operation in that, and then incorporating that tweaked mixin into the ultimate window.
I hope the above helps better illustrate the use and flexibility of the Ada 95 type composition building blocks.
(Tucker Taft) |