======= LSN007.ImplConv ======= !topic Implicit Conversions and Class-Wide Operations !from Tucker Taft 91-05-31 !reference MD-4.6(9-16);2.0 !reference MD-4.6.1;2.0 !discussion There has been some concern that the OOP features of Ada 9X are introducing a confusing set of implicit conversions, coupled with an arbitrary set of restrictions. This LSN will attempt to explain the role of implicit conversion, and the motivation behind the restrictions. It would be wise to read the LSN on Types, Classes, and Class-wide operations first, though it is not strictly necessary. CLASS-WIDE OPERATIONS The productivity gain from OOP features comes largely from the ability to define classes of related types, and then define class-wide operations non-redundantly, as high up in the class hierarchy as possible. This can result in significant code-size reductions for a large system with lots of types. The ability to define an operation which applies to any type in a class seems to conflict with the normal strong-type-checking rules of a language like Ada. The "tricks" used to get around this are either inheritance (with "implicit explicit" conversion of parameters when using an inherited subprogram), "explicit implicit" conversion as provided for operations returning universal-integer in Ada 83, or via generics with formal type parameters identifying the class of applicability of the generic. Inheritance and generics by themselves are somewhat limiting. Inheritance is the way for establishing the primitive class-wide operations, and implementing them at the appropriate level in the class hierarchy. However, given the Ada-83 semantics for inheritance as part of type derivation, a primitive operation cannot "redispatch" in its implementation to take advantage of possible overridings of other primitive operations. This is because the parameters are converted before calling an inherited operation, and the original type of the parameter is no longer available. On the other hand, generics are limiting in a different way in Ada. They require explicit instantiation before use, and require compile-time knowledge of the specific type of interest. Therefore, we feel it is important to provide the ability to define class-wide operations explicitly, so that they can be called with actual parameters of any type in the class. If these operations are functions, then it is also important that the particular result type can be inferred from context (with a run-time check if tagged). We have chosen the "trick" of a using a universal type for the class to allow these class-wide operations to be explicitly defined. When a formal parameter is of the universal type, then the actual parameter may be of any type in the class. When the result type is the univeral type, then the actual type is determined from context (the result is effectively overloaded on all types in the class). Formally, this is considered an implicit conversion to or from the universal type. From the user's perspective, however, this operation is simply a class-wide operation. IMPLEMENTING A CLASS-WIDE OPERATION When writing the body of an explicitly declared class-wide operation, a formal parameter and/or the result is considered to be of the universal type for the class. To do anything useful with these parameters, we need to know what operations are available on them. The obvious answer is that all class-wide operations are available on the universal type. The class-wide operations available for use on the universal type include the primitive operations of the root-type (with appropriate substitutions), explicitly declared class-wide operations (like the one being implemented), and generics with a formal type identifying the class. This principle makes it clear that the universal type should itself be considered part of the class, as well as a part of any enclosing "super" class. With Ada we must always ask "where" a given operation is declared, so we can know its visibility. The simplest answer for the class-wide operations is as follows: a) the primitive operations are declared where the root-type is declared b) the explicitly-declared class-wide operations are declared explicitly, and they are never implicitly inherited into any other scope. c) a generic is similarly declared explicitly, and not implicitly inherited anywhere else. RESTRICTIONS ON CLASS-WIDE OPERATIONS There are a some restrictions on the use of class-wide operations to keep things unambiguous and meaningful. First of all, the implicitly declare primitive operations of the universal type are *not* considered class-wide operations (despite their implicit parameter profile). They are usable *only* on the universal type itself, and return a value which is of the universal type only, and not implicitly convertible to any other type in the class (see MD-3.4.4(15);2.0 and MD-4.6.1(3);2.0). This is analagous to the Ada 83 rules for the operations on the universal numeric types. The universal integer "+" take only universal integers, and the result of "+" cannot be implicitly converted to any other type (see RM-4.6(15)). In both cases, the purpose of the rule is to avoid ambiguity between the universal primitive operations and the corresponding primitive operations on the non-universal types. However, the explicitly declared operations with parameters and/or result of the universal type *are* considered class-wide operations. A further limitation applies to the implicitly declared primitive operations which have a result of the universal type, but no operands of the universal type. Even after disallowing implicit conversions (i.e. not considering them class-wide operations), these primitive operations can be ambiguous with the corresponding operation of the root type. For example, imagine a root type T, with a primitive operation: function Construct(N : Integer) return T; It should be unambiguous to write: if Construct(1) = Construct(2) then ... However, if the corresponding primitive operation on T'CLASS is included in the possible interpretations, then this becomes ambiguous. Therefore, the Mapping Document introduces the limitation that such operations on the universal type are not considered unless the context identifies the universal type uniquely (see MD-3.4.4(16);2.0 and MD-4.8.3(4);2.0). For example, this would be a legal use of the primitive operation Construct which returns T'CLASS, in defining a new class-wide operation: procedure Build(X : out T'CLASS; N : Integer) is begin X := Construct(N); end Build; ADDITIONAL RESTRICTIONS The following additional restrictions exist for implicit conversions: 1) Scope-based restriction a) May not implicitly or explicitly convert from T1 to T'CLASS if T is tagged, and T1 is declared at an inner scope relative to where T is declared (MD 4.6(11);2.0). This restriction prevents a tagged universal value with a tag potentially identifying operations declared at an inner scope, from escaping the scope. This restriction could be modified a bit so as to only disallow allocators or assignments which might result in a tag outlasting its scope. However, this would then become a run-time check. If we eliminate tagged elementary types, then only allocators would require the run-time check, because tag-changing assignments are disallowed on tagged composite types. 2) Sensibility restrictions a) May not implicitly or explicitly convert from T1 to T'CLASS if T is tagged and non-limited and T1 is limited (MD 4.6(13);2.0). This prevents a type from becoming assignable via a conversion to its universal type. For example, imagine a task is included in the record extension to some tagged type. This extension is therefore limited. If we were to allow it to be converted to a non-limited universal type then we would have no protection against assignment overriding this task component. b) May not implicitly or explicitly convert from T1 to T'CLASS if T is tagged and elementary, and T1 is composite (MD 4.6(14);2.0) This means that if by some strange means (e.g. via extending a generic formal type, or via extending a private type which is implemented via a tagged elementary type) a "mongrel" type is created which is a record extension of an elementary type, then the mongrel is not expected to "fit" into the tagged elementary universal type. This restriction ensures that tagged elementary universal types have an upper bound on their size, allowing them to be reassigned freely without requiring dynamic reallocation of space. This restriction is moot if we eliminate tagged elementary types. c) May not implicitly or explicitly convert from a tagged T'CLASS to T1 or T1'CLASS if the tag of the universal value identifies a type which is not in the class rooted at T1 (MD 4.6(15);2.0). This restriction is due in part to sensibility, in part to methodological concerns, and in part to implementation issues. Because the value being converted is of a tagged universal type, we don't know its tag at compile time. Therefore, if the target requires a possible extension, we would be extending from an unknown type, requiring an unknown amount of default initializations of an unknown set of new components. However, so long as the target type requires only a chopping back, all we need to do is change the tag. If the target "type" is itself universal (e.g. T1'CLASS), then all we need to do is ensure that the current tag identifies a type in the class rooted at T, and then no change whatsoever to the value is necessary. 3) Methodological restrictions a) May not implicitly or explicitly convert from T1 to T'CLASS if T is tagged and T1 overrides one of T's primitive operations which is non-dispatching for the universal type(MD 4.6(12);2.0). This is only relevant for tagged elementary types, since all primitive operations on tagged composite types are dispatching for the universal type. The predefined operations on an elementary type are the ones which are non-dispatching for the elementary type. This restriction is present so that operations applied to an instance of T'CLASS will have semantics drawn from some single non-universal type. If T1 overrides an operation which is non-dispatching, then when it is converted to T'CLASS it will be using the "wrong" operation, which may produce meaningless results when combined with other dispatching operations. This restriction is moot if we eliminate tagged elementary types. It could also be dropped because it is more complex than it is worth. b) May not implicitly convert from T'CLASS to T1 if T1 has more components than the current value of T (MD 4.6.1(2);2.0). This goes along with our requirement that functions returning a type must be overridden for an extension. This corresponds to the general OOP rule that "constructors" cannot be inherited in extensions. It seemed potentially confusing to have this kind of implicit value extension. One could allow this in cases where the explicit conversion is legal (i.e. when all components have defaults). This should probably remain consistent with the rule for inherited functions (even though there it involves an "implicit explicit" conversion rather than an "explicit implicit" conversion). SUMMARY Implicit conversion is essential to allow the definition of new class-wide operations, if they are based on the "trick" of having a formal parameter or result type of a universal T'CLASS type. There are four kinds of restrictions imposed on the conversions to and from universal types: 1) Avoiding dangling references 2) Avoiding meaningless situations 3) Preserving certain methodological goals 4) Avoiding ambiguity The methodological restrictions could be dropped or revised, but there should be a consistent decision of when implicit value extension is permitted. Two of the restrictions (2b and 3a) are only applicable to tagged elementary types, and so if we decide to drop them, then those restrictions can go. Hopefully, the restrictions which remain can be described in a way which conveys more of the rationale behind the restrictions, and makes them more "intuitive" to the programmer.