NOTE: This proposal for constructors and user-defined conversions +does not represent the current state of Cforall language development, but is +maintained for its possible utility in building user-defined conversions. See +doc/proposals/user_conversions.md for a more current presentation of +these ideas.
+ +This is the first draft of a description of a possible extension to the +current definition of Cforall ("Cforall-as-is") that would let programmers +fit new types into Cforall's system of conversions.
+ +My design goal for this extension is to provide a framework that +explains the bulk of C's conversion semantics in terms of more basic +languages, just as Cforall explains most expression semantics in terms of +overloaded function calls.
+ +My pragmatic goal is to allow a programmer to define a portable rational +number data type, fit it into the existing C type system, and use it in +mixed-mode arithmetic expressions, all in a convenient and esthetically +pleasing manner.
+ +A conversion creates a value from a value of a different +type. C defines a large number of conversions, especially between +arithmetic types. A subset of these can be performed by implicit +conversions, which occurs in certain contexts: in assignment +expressions, when passing arguments to function (where parameters are +"assigned the value of the corresponding argument"), in initialized +declarations (where "the same type constraints and conversions as for +simple assignment apply"), and in mixed mode arithmetic. All conversions +can be performed explicitly by cast expressions.
+ +C prefers some implicit conversions, the promotions, to the +others. The promotions are ranked among themselves, creating a hierarchy +of types. In mixed-mode operations, the "usual arithmetic conversions" +promote the operands to what amounts to their least common supertype. +Cforall-as-is uses a slightly larger set of promotions to choose the +smallest possible promotion when resolving overloading.
+ +An extension should allow Cforall to explain C's conversions as a set of +pre-defined functions, including its explicit conversions, implicit +conversions, and preferences among conversions. The extension must let the +programmer define new conversions for programmer-defined types, for +instance so that new arithmetic types can be used conveniently in +mixed-mode arithmetic.
+ +C++ introduced constructors to the C language family, and I will use its +terminology. A constructor is a function that initializes an +object. C does not have constructors; instead, it makes do with +initialization, which works like assignment. Cforall-as-is does not have +constructors, either: instead, by analogy with C's semantics, a +programmer-defined assignment function may be called during initialization. +However, there is a key difference between a function that implements +assignment and a constructor: constructors assume that the object is +uninitialized, and must set up any data structure invariants that the +object is supposed to obey. An assignment function assumes that the target +object obeys its invariants.
+ +A default constructor has no parameters other than the object it +initializes. It establishes invariants, but need not do anything else. A +default constructor for a rational number type might set the denominator to be +non-zero, but leave the numerator undefined.
+ +A copy constructor has two parameters: the object it +initializes, and a value of the same type. Its purpose is to copy the +value into the object, and so it is very similar to an assignment +function. In fact, it could be expressed as a call to a default constructor +followed by an assignment.
+ +A converting constructor also has two parameters, but the +second parameter is a value of some type different from the type +of the object it initializes. Its purpose is to convert the value to the +object's type before copying it, and so it is very similar to a C +assignment operation that performs an implicit conversion.
+ +C++ sensibly defines parameter passing as call by initialization, since +the parameter is uninitialized when the argument value is placed in it. +Extended Cforall should do the same. However, parameter passing is one of +the main places where implicit conversions occur. Hence in extended +Cforall constructors define the implicit conversions. Cforall +should also encourage programmers to maintain the similarity between +constructors and assignment.
+ +In extended Cforall, programmer-defined conversions should fit in with +the predefined conversions. For instance, programmer-defined promotions +should interact with the normal promotions so that programmer-defined types +can take part in mixed-mode arithmetic expressions. The first design that +springs to mind is to define a minimal set of conversions between +neighbouring types in the type hierarchy, and to have Cforall create +conversions between more distant types by composition of predefined and +programmer-defined conversions. Unfortunately, if one draws a graph of C's +promotions, with C's types as vertices and C's promotions as edges, the +result is a directed acyclic graph, not a tree. This means that an attempt +to build the full set of promotions by composition of a minimal set of +promotions will fail.
+ + Consider a simple processor with 32-bit int
and
+long
types. On such a machine, C's "usual arithmetic
+conversions" dictate that mixed-mode arithmetic that combines a signed
+integer with an unsigned integer must promote the signed integer to an
+unsigned type. Here is a directed graph showing the some of the minimal
+set of promotions. Each of the four promotions is necessary, because each
+could be required by some mixed-mode expression, and none can be decomposed
+into simpler conversions.
+long --------> unsigned long + ^ ^ + | | +int ---------> unsigned int ++ +
Now imagine attempting to compose an int
-to-unsigned
+long
conversion from the minimal set: there are two paths through
+the graph, so the composition is ambiguous.
(In C, and in Cforall-as-is, no ambiguity exists: there is just one
+int
-to-unsigned long
promotion, defined by the
+language semantics. In Cforall-as-is, the preference for
+int
-to-long
over
+int
-to-unsigned long
is determined by a
+"conversion cost" calculated from the graph of the full set of promotions,
+but the calculation depends on maximal path lengths, not the exact
+path.)
Unfortunately, the same problem with ambiguity creeps in any time +conversions might be chained together. The extension must carefully +control conversion composition, so that programmers can avoid ambiguous +conversions.
+ +The rest of this document describes my proposal to add +programmer-definable conversions and constructors to Cforall.
+ +If your browser supports CSS style sheets, the +proposal will appear in "normal" paragraphs, and commentary on the proposal +will have the same appearance as this paragraph.
+ +Cforall would be given a cast identifier, two +constructor identifiers, and a destructor +identifier:
+ +(?)?
, for cast functions.(?create)?
, for constructors.(?promote)?
, for constructors that are promotions.(?destroy)?
, for destructors.The ugly identifier (?)?
is meant to be mnemonic for the
+cast expression. The other identifiers are pretty weak (suggestions,
+anyone?) but are supposed to remind the programmer of the connection
+between conversions and constructors.
We could instead use a single (?create)?
identifier for
+constructors and add a promote
storage class specifier, at
+some small risk clashes of with identifiers in existing code.
It is an error to declare two functions with different constructor +identifiers that have the same type in the same translation unit.
+ +Functions declared with these identifiers can be polymorphic. Unlike +other polymorphic functions, the return type of a polymorphic cast function +need not be derivable from the type of its parameters
+ +The return type of a call to a polymorphic cast +function can be deduced from the calling context.
+ ++forall(type T1) T1 (?)?(T2); // Legal. +forall(type T1) T1 pfun(T2); // Illegal -- no way to infer T1. ++
A cast function from type T1
to type
+T2
is named "(?)?
", accepts exactly one explicit
+argument of type T1, and returns a value of type T2.
If the cast function is polymorphic, it will have +type parameters and assertion parameters as well, and can be said to be a +cast function from many different types to many different types. +
+ +A default constructor function for type T is named
+"(?create)?
", accepts exactly one explicit argument of type
+T*
, and returns void
.
A copy constructor function for type T is named
+"(?create)?
", accepts exactly two explicit arguments of types
+T*
and T, and returns void
.
A converting constructor function for type T1 from
+T2 is named "(?create)?
" or "(?promote)?
",
+accepts exactly two explicit arguments of types T1*
and
+T2, and returns void
.
A destructor function for type T is named
+"(?destroy)?
", accepts exactly one explicit argument of type
+T*
, and returns void
.
The monomorphic function prototypes for these functions are
++T1 (?)?(T2); +void (?create)?(T1*); +void (?create)?(T1*, T2); +void (?promote)?(T1*, T2); +void (?destroy)?(T1*); ++
In most cases the cast expression (T)e
would
+be treated like the function call (?)?(e)
, except that
+only cast functions to type T would be valid interpretations of
+(?)?
, and e
would not be implicitly
+converted to the cast function's parameter type. In particular, the usual
+rules for resolving function overloading (see below)
+would be used to choose the best interpretation of the expression.
For example, in
++type Wazzit; +type Thingum; +Wazzit w; +(Thingum)w; ++
the cast function that is called must be "Thingum
+(?)?(Wazzit)
", or a polymorphic function that can be specialized to
+that.
The ban on implicit conversions within the cast allows programmers to +explicitly control composition of conversions and avoid ambiguity. I also +hope that this will make it easier for compilers and programmers to +determine which conversions will be applied in which circumstances. If +implicit conversions could be applied to the inputs and outputs of casts, +when any and all of the conversion functions involved could be polymorphic +... the possibilities seem endless, unfortunately.
+ +A definition of an object x would call a constructor function. Let
+T be x's type with type qualifiers removed, and let a
+be x's address (with type T*
).
If type qualifiers weren't ignored, const
+objects couldn't be initialized, and every constructor would have to be
+duplicated, with one version for T* objects and one for
+volatile
T* objects.
f(a,e)
, except that only copy and
+ converting constructors for type T would be valid interpretations
+ of f
, and e would not be implicitly
+ converted to the type of the constructor's second parameter.f(a)
, except that only default
+ constructor functions for type T would be valid interpretations of
+ f
.If x has static storage duration and is not initialized + explicitly, and is defined within the scope of a type definition that + defines T, then T's implementation type would determine how + x is initialized.
++ type Rational = struct { int numerator; unsigned denominator; }; + Rational r; // Both members initialized to 0. ++
If x has static storage duration and is not initialized
+ explicitly, and the type T is an opaque type, the definition
+ would be treated as if x was initialized with the expression
+ 0
.
This is a simple extension of C's rules for static objects, + which initialized them all to 0. Frequently, the 0 involved + will have type T, and the definition will call a copy + constructor.
++ extern type Rational; + extern Rational 0; + static Rational r; // initialized with the Rational 0. ++
In other cases, the 0 will be an integer or null pointer, and the + definition will call a converting constructor.
+ +The obvious alternative design would call T's default
+ constructor. That design would be inconsistent, because some static
+ objects would go uninitialized. It would also cause subtle problems,
+ because a particular static definition could be uninitialized or
+ initialized to 0 depending on whether T is an extern
+ type
or a typedef
.
Except when calling constructors, parameter passing invokes constructor +functions. Passing argument expression e to a parameter would be +equivalent to initializing the parameter with that expression. When +calling constructors, the value of the argument would be copied into the +parameter.
+ +When the lifetime of x ends, a destructor function would be called.
+The call would be treated much like the function call
+(?destroy)?(a)
. When a block ends, the objects that were
+defined in the block would be destroyed in the reverse of the order in which
+they are declared.
The storage class specifier register
will have the
+semantics that it has in C++, instead of the semantics of C: it is merely a
+hint to the implementation that the object will be heavily used, and does
+not prevent programs from computing the address of the object.
In Cforall-as-is, every declaration with type-class type
+implicitly declares a default assignment function, with the same scope and
+linkage as the type. Extended Cforall would also declare a default
+default constructor and a default destructor.
+{ + extern type T; + T t; // calls external constructor for T. + } // calls external destructor for T. ++
The destructor and some sort of constructor are necessary to instantiate +the type. I include the default constructor because it is the most basic. +Arguably the declaration should also declare a default copy constructor, +but I chose not to because Cforall can construct a copy constructor from +the default constructor and the assignment operator, as will be seen +below.
+ +If the type does not need to be instantiated, it probably should have
+been declared by dtype
instead of by type
.
A type definition would implicitly define a default constructor and +destructor by inheriting the implementation type's default constructor and +destructor, just as is done for the implicitly defined default assignment +function.
+ +As mentioned above, Cforall does not apply implicit conversions to the +arguments and results of cast expressions or constructor calls. Neither +does it automatically create conversions or constructors by composing +programmer-defined compositions: given
+ ++T1 (?)?(T2); +T2 (?)?(T3); +T3 v3; +(T1)v3; ++ +
then Cforall does not automatically create
+ ++T1 (?)?(T3 p) { return (T1)(T2)p; } ++ +
Composition of conversions does show up through a third mechanism where
+the programmer has more control: assertion lists. Consider a
+Month
type, that represents months as integers between 0 and
+11. Clearly a Month
can be promoted to unsigned
,
+and to any type above unsigned
in the arithmetic type
+hierarchy as well.
+type Month = unsigned; + +forall(type T | void (?promote)(T*, unsigned)) + void (?promote)?(T* target, Month source) { + unsigned u_temp = (unsigned)source; + T t_temp = u_temp; // calls the assertion parameter. + *target = t_temp; + } ++ +
The intimidating polymorphic promotion declaration says that, if
+T
is a type and unsigned
can be promoted to
+T
, then the function can promote Month
to
+T
.
+Month m; +unsigned long ul = m; ++ +
To initialize ul
, Cforall must bind T
to
+unsigned long
, find the (pre-defined)
+unsigned
-to-unsigned long
promotion, and pass it
+to the assertion parameter of the polymorphic
+Month
-to-T
function.
But what about converting from Month
to
+unsigned
itself?
+unsigned u = m; // How? ++ +
A monomorphic Month
-to-unsigned
constructor
+would do the job, but its body would mostly duplicate the body of the
+polymorphic function.
Instead, Cforall should use the polymorphic promotion and the
+unsigned
copy constructor. To initialize u
,
+Cforall should pass the unsigned
copy constructor to the assertion
+parameter of the polymorphic Month
promotion, and bind
+T
to unsigned
.
Note that the polymorphic promotion can promote Month
to
+the standard types, to implementation-defined extended types, and to
+programmer-defined types that have yet to be written. This is much better
+than writing a flock of monomorphic promotions, with function bodies that
+would be nearly identical, to convert Month
to each unsigned
+type individually. The predefined constructors make heavy use of this
+constructor idiom: instead of writing
+void (?promote)? (T1*, T2); ++ +
("You can make a T2 into a T1"), write
++forall(type T | void (?promote)?(T*, T1) ) void (?promote)?(T*, T2); ++ +
("You can make a T2 into anything that can be made from a T1").
+ +Calls to constructors have construction costs, which let +Cforall choose the least expensive implicit conversion when given a +choice.
+ +Note that, although point 3 refers to constructors that are +passed at run-time, the translator statically matches arguments to +assertion parameters, so it can determine construction costs statically.
+ +Construction cost is defined for every +constructor, not just the promotions (which are the equivalent of the safe +conversions of Cforall-as-is). This seemed like the easiest way to handle +(admittedly dicey) "mixed" constructors, where the constructor and its +assertion parameter have different identifiers:
+ ++type Thingum; +type Wazzit; +forall(type T | void (?create)?(T*, Thingum) ) + void (?promote)?(T*, Wazzit); ++
"unsigned ui = 42U;
" calls a copy constructor, and so has
+cost 0.
"unsigned ui = m;
", where m
has type
+Month
, calls the polymorphic Month
promotion
+defined previously. It passes the
+unsigned
-to-unsigned
copy constructor to the
+assertion parameter, and so has cost 1+0 = 1.
"unsigned long ul = m;
" calls the polymorphic
+Month
promotion, passing the
+unsigned
-to-unsigned long
constructor to the
+assertion parameter. unsigned
-to-unsigned long
+is defined below and will turn out to have cost 1, so the total cost is 2.
Inside the body of the Month
promotion, the assertion
+parameter has a monomorphic type, and so has a construction cost of 1 where
+it is called by the initialization of t_temp
. The cost of the
+argument passed through the assertion parameter has no relevance
+inside the body of the promotion.
In Cforall-as-is, there is at most one language-defined implicit +conversion between any two types. In extended Cforall, more than one +conversion may be applicable, and overload resolution must be adapted to +account for that, by using the lowest-cost conversion.
+ +The unsafe conversion cost of a function call expression
+would be the total conversion cost of implicit calls of
+(?create)?()
constructors applied directly to arguments of the
+function -- 0 if there are none.
This would replace a rule in Cforall-as-is, which +considers all unsafe conversions to be equally bad and just counts them. I +think the difference would be subtle and unimportant.
+ +The promotion cost would be the total conversion costs of
+implicit calls of (?promote)?()
constructors applied directly
+to arguments of the function -- 0 if there are none.
Overload resolution would examine each argument expression individually. +The best interpretations of an expression would be:
+ +The best interpretation would be implicitly converted to the parameter +type, by calling the conversion function with minimal cost. If there is +more than one best interpretation, or if there is more than one +minimal-cost conversion, the argument is ambiguous.
+ +A maximal set of interpretations of the function call expression that +have compatible result types produces a single interpretation: the +interpretations with the lowest unsafe conversion cost, and of these, the +interpretations with the lowest promotion cost. If there is more than one +such interpretation, the function call expression is ambiguous.
+ +Cforall would define new heap allocation functions that would ensure +that constructors and destructors would be applied to objects in the +heap. There's lots of room for ambitious design here, but a simple +facility might look like this:
+ ++forall(type T) void delete(T const volatile restrict* ptr) { + if (ptr) (?destroy)?(ptr); + free(ptr); +} ++ +
In a call to delete()
, the argument might be a pointer to a
+pointer: T
would be a pointer type, and the argument might
+have all three type qualifiers. (If it doesn't, pointer conversions will add
+missing qualifiers to the argument.)
+// Pointer to a const volatile restricted pointer to an int: +int * const volatile restrict * pcvrpi; +// ... +delete(cvrpi); // T bound to int * ++
A new()
function would take the address of a pointer and an
+initial value, and points the pointer at heap storage initialized to that
+value.
+forall(type T | void (?create)?(T*, T)) + void new(T* volatile restrict* ptr, T val) { + *ptr = malloc(sizeof(T)); + if (*ptr) (?create)?(*ptr, val); // explicit constructor call +} + +forall(type T | void (?create)?(T*, T)) + void new(T const* volatile restrict* ptr, T val), + new(T volatile* volatile restrict* ptr, T val), + new(T restrict* volatile restrict* ptr, T val), + new(T const volatile* volatile restrict* ptr, T val), + new(T const restrict* volatile restrict* ptr, T val), + new(T volatile restrict* volatile restrict* ptr, T val), + new(T const volatile restrict* volatile restrict* ptr, T val); ++
Cforall can't add type qualifiers to pointed-at
+pointer types, so new()
needs one variation for each set of
+type qualifiers.
Another new()
function would omit the initial value, and
+apply the default constructor. Obviously, there's
+no point in allocating const
-qualified uninitialized
+storage.
+forall(type T) + void new(T* volatile restrict * ptr) { + *ptr = malloc(sizeof(T)); + if (*ptr) (?create)?(*ptr); // Explicit default constructor call. +} + +forall(type T) + void new(T volatile* volatile restrict*), + void new(T restrict* volatile restrict*), + void new(T volatile restrict* volatile restrict*); ++ +
Cforall would provide a polymorphic default constructor function and +destructor function, for types that do not have their own:
+ ++forall(type T) + void (?create)?(T*) { return; }; + +forall(type T) + void (?destroy)?(T*) { return; }; ++ +
The generic default constructor and destructor provide +C semantics for uninitialized variables: "do nothing".
+ +For every structure type struct s
Cforall would define a
+default constructor function that applies a default constructor to each
+member, in no particular order. Similarly, it would define a destructor that
+applies the destructor of each member in no particular order.
Any promotion would be treated as a plain constructor:
++forall(type T, type S | void (?promote)(T*, S)) + void (?create)?(T*, S) { + (?promote)?(T*, S); // Explicit constructor call! + } ++ +
A predefined cast function would allow explicit conversions anywhere +that implicit conversions are possible:
++forall(type T, type S | void (?create)?(T*, S)) + T (?)?(S source) { + T temp = source; + return temp; + } ++ +
A predefined converting constructor would allow initialization anywhere +that assignment is defined:
++forall(type T | void (?create)?(T*), type S | T ?=?(T*, S)) + void (?create)?(T* target, S source) { + (?create)?(target); + *target = source; + } ++ +
This implements the typical semantic link between +assignment and initialization.
+ +The predefined copy constructor function is
++forall(type T) + void (?promote)?(T* target, T source) { + (?create)?(target); + *target = source; + } ++ +
Since Cforall defines assignment and default +constructors for structure types, this provides the copy constructor for +structure types.
+ +Finally, Cforall defines the conversion to void
, which
+discards its argument.
+forall(type T) void (?promote)(void*, T); ++ +
C has five groups of arithmetic types: signed integers, unsigned
+integers, complex floating-point numbers, imaginary floating-point numbers,
+and real floating-point numbers. (Implementations are not required to
+provide complex and imaginary types.) Some of the "usual arithmetic
+conversions" promote upward within a group or to a more general group: from
+int
to long long
, for instance. Others
+promote across from a type in one group to a similar type in another group:
+for instance, from int
to unsigned int
.
The floating point types would use the constructor +idiom for upward promotions, and monomorphic constructors for +promotions across from real and imaginary types to complex types with the +same precision.
+ +I will use a macro to abbreviate the constructor idiom.
+"Promoter(T,S)
" promotes S
to any type that
+T
can be promoted to
+#define Promoter(Target, Source) \ + forall(type T | void (?promote)?(T*, Target)) void (?promote)?(T*, Source) + +Promoter(long double _Complex, double _Complex); // a +Promoter(double _Complex, float _Complex); // b +Promoter(long double, double); // c +Promoter(double, float); // d +Promoter(long double _Imaginary, double _Imaginary); // e +Promoter(double _Imaginary, float _Imaginary); // f + +void (?promote)?(long double _Complex*, long double); // g +void (?promote)?(long double _Complex*, long double _Imaginary); // h +void (?promote)?(double _Complex*, double); // i +void (?promote)?(double _Complex*, double _Imaginary); // j +void (?promote)?(float _Complex*, float); // k +void (?promote)?(float _Complex*, float _Imaginary); // l ++ +
It helps to draw a graph of the promotions. In this diagram, +monomorphic promotions are solid arrows from the source type to the target +type, and polymorphic promotions are dotted arrows from the source type to +a bubble that surrounds all possible target types. (Twenty years after +first hearing about them, I have finally found a use for directed +multigraphs!) To determine the promotion from one type to another, find a +path of zero or more dotted arrows optionally ending with a solid arrow.
+A long double _Complex
can be constructed from
double _Complex
, via a, with a double
+ _Complex
copy constructor passed as the assertion
+ parameter.long double
, via constructor g.double
, via c (which promotes
+ double
to long double
and higher), with
+ g passed as the assertion parameter. In other words, the path
+ from double
to long double _Complex
passes
+ through long double
float _Complex
, via b. For the assertion
+ parameter, Cforall passes a double
+ _Complex
-to-long double _Complex
constructor that
+ it makes by specializing a; for the assertion parameter of the
+ specialization, it passes a long double
+ _Complex
-to-long double _Complex
copy
+ constructor.float
, via d, with a specialization of c
+ passed as its assertion parameter, with g passed as the
+ specialization's assertion parameter.Note how "upward" and "across" promotions interact. Polymorphic +"upward" promotions connect widely separated types by composing +constructors through their assertion parameters. Monomorphic "across" +promotions extend composition one step across to corresponding types in +different groups.
+ +Defining the set of predefined promotions turned out to be quite tricky.
+For example, if "across" promotions used the constructor idiom, ambiguity
+would result: a conversion from float
to double
+_Complex
could convert upward through double
or across
+through float _Complex
. The key points are:
The conversions for the integer types cannot be +defined by a simple list, because the set of integer types is +implementation-defined, the range of each type is implementation-defined, +and the set of promotions depend on whether a particular signed type can +represent all values of a particular unsigned type. As I read the C +standard, every signed type has a matching unsigned type, but the reverse +is not true. This complicates the definitions below.
+ +int
.signed(r)
and
+ unsigned(r)
+ be the signed integer type and unsigned integer type with rank
+ r.Integers promote upward to floating-point types. Let SMax be the +highest ranking signed integer type, and let UMax be the highest +ranking unsigned integer type. Then Cforall would define
+ ++Promoter(float, SMax); +Promoter(float, Umax); ++ +
Signed types promote across to unsigned types with the same rank. For
+every r >= r_{int} such that
+signed(r)
exists, Cforall would define
+void (?promote)?( unsigned(r)*, signed(r) ); ++ +
Lower-ranking signed integers promote to higher-ranking signed integers. +For every signed integer type T with rank greater than +r_{int}, let S be the signed integer type with the +next lowest rank. Then Cforall would define
+ ++Promoter(T, S); ++ +
Similarly, lower-ranking unsigned integers promote to higher-ranking +unsigned integers. For every r > r_{int}, Cforall +would define
+ ++Promoter(unsigned(r), unsigned(r-1)); ++ +
C's usual arithmetic conversions may promote an unsigned type to a
+signed type, but only if the signed type can represent every value of the
+unsigned type. For every r >= r_{int}, if there
+are any signed types that can represent every value in
+unsigned(r)
, let S be the
+lowest ranking of these types; then Cforall defines
+Promoter(S, unsigned(r)); ++ +
C's integer promotions apply to "small" types (those with
+rank less than r_{int}): they promote to int
if
+int
can hold all of their values, and to unsigned
+int
otherwise. At least one unsigned type, _Bool
,
+will promote to int
. This breaks the pattern set by the usual
+arithmetic conversions, where unsigned types always promote to the next
+larger unsigned type. Consider a machine with 32-bit int
s and
+16-bit unsigned short
s: if two unsigned short
s
+are added, they must be promoted to int
instead of
+unsigned int
. Hence for this machine there must not
+be a promotion from unsigned short
to unsigned
+int
.
Since the C integer promotions always promote small signed types to
+int
, Cforall would extend the chain of polymorphic "upward"
+and monomorphic "across" signed integer promotions to the small
+signed types.
For every signed integer type S with rank less than +r_{int}, Cforall would define
++Promoter(T, S); ++
where T is the signed integer type with the next highest +rank.
+ +Let r_{break} be the rank of the highest-ranking unsigned
+type whose values can all be represented by int
, and let
+T be the lowest-ranking signed type that can represent all of the
+values of unsigned(r_{break})
. Cforall would
+define
+Promoter(T, unsigned(r_{break})); ++ +
For every +r less than r_{int} except +r_{break}, Cforall would define
++Promoter(unsigned(r+1), unsigned(r)); ++ +
r_{break} is the point where the normal
+pattern of unsigned promotion breaks. Unsigned types with higher rank
+promote upward toward unsigned int
. Unsigned types with
+lower rank promote upward to the type at the break, which promotes upward
+to a signed type and onward toward int
.
For each r < r_{int} such that
+signed(r)
exists, Cforall would define
+void (?promote)?(unsigned(r)*, signed(r)); ++ +
These "across" promotions are not strictly necessary,
+but it seems useful to extend the pattern of signed-to-unsigned monomorphic
+conversions established by the larger integer types. Note that because of
+these promotions, unsigned(r_{break})
does
+promote to the next larger unsigned type, after a detour through a signed
+type that increases the conversion cost.
Finally, char
is equivalent to signed char
or
+unsigned char
, on an implementation-defined basis. If
+char
is equivalent to signed char
, the
+implementation would define
+Promoter(signed char, char); ++ +
Otherwise, it would define
++Promoter(unsigned char, char); ++ +
Promotions can add qualifiers to the pointed-to type of a +pointer type.
++forall(dtype DT) void (?promote)?(const DT**, DT*); +forall(dtype DT) void (?promote)?(volatile DT**, DT*); +forall(dtype DT) void (?promote)?(restrict DT**, DT*); +forall(dtype DT) void (?promote)?(const volatile DT**, DT*); +forall(dtype DT) void (?promote)?(const restrict DT**, DT*); +forall(dtype DT) void (?promote)?(volatile restrict DT**, DT*); +forall(dtype DT) void (?promote)?(const volatile restrict DT**, DT*); + +forall(dtype DT) void (?promote)?(const volatile DT**, const DT*); +forall(dtype DT) void (?promote)?(const restrict DT**, const DT*); +forall(dtype DT) void (?promote)?(const volatile restrict DT**, const DT*); + +forall(dtype DT) void (?promote)?(const volatile DT**, volatile DT*); +forall(dtype DT) void (?promote)?(volatile restrict DT**, volatile DT*); +forall(dtype DT) void (?promote)?(const volatile restrict DT**, volatile DT*); + +forall(dtype DT) void (?promote)?(const restrict DT**, restrict DT*); +forall(dtype DT) void (?promote)?(volatile restrict DT**, restrict DT*); +forall(dtype DT) void (?promote)?(const volatile restrict DT**, restrict +DT*); + +forall(dtype DT) void (?promote)?(const volatile restrict DT**, const volatile DT); +forall(dtype DT) void (?promote)?(const volatile restrict DT**, const restrict DT); +forall(dtype DT) void (?promote)?(const volatile restrict DT**, volatile restrict DT); ++ +
The type qualifier promotions are simple, but verbose +because Cforall doesn't abstract over type qualifiers very well. They also +give every type qualifier promotion a cost of 1. It is possible +to define a smaller set of promotions, some using the constructor idiom, +that gives greater cost to promotions that add more qualifiers, but the set +is arbitrary and asymmetric: only one of the three promotions that add one +qualifier to an unqualified pointer type can use the constructor idiom, or +else ambiguity results.
+ +Within the scope of a type definition type T1 =
+T2;
, constructors would convert between the new type and its
+implementation type.
+void (?promote)(T2*, T1); +void (?promote)(T2**, T1*); +void (?create)?(T1*, T2); +void (?create)?(T1**, T2*); ++ +
The conversion from the implementation type
+T2
to the new type T1
gives
+functions that implement operations on T1
access to the
+type's implementation. The conversion is a promotion because most such
+functions work with the implementation most of the time. The reverse
+conversion is merely implicit, so that mixed operations won't be
+ambiguous.
C defines implicit conversions between any two +arithmetic types. In Cforall terms, the conversions that are not +promotions are ordinary conversions. Most of the ordinary conversions +follow a pattern that looks like the Usual Arithmetic +Conversions in reverse. Once again, I will use a macro to hide details +of the constructor idiom.
+ ++#define Creator(Target, Source) \ + forall(type T | void (?create)?(T*, Target)) void (?create)?(T*, Source) + +Creator(double _Complex, long double _Complex); +Creator(float _Complex, double _Complex); +Creator(double, long double); +Creator(float, double); +Creator(double _Imaginary, long double _Imaginary); +Creator(float _Imaginary, double _Imaginary); + +void (?create)?(long double*, long double _Complex); +void (?create)?(long double _Imaginary*, long double _Complex); +void (?create)?(double*, double _Complex); +void (?create)?(double _Imaginary*, double _Complex); +void (?create)?(float*, float _Complex); +void (?create)?(float _Imaginary*, float _Complex); ++ +
The C99 draft standards that I have access to state +that real types and imaginary types are implicitly interconvertible. This +seems like a mistake, since the result of the conversion will always be +zero, but ...
+ ++void (?create)?(long double*, long double _Imaginary); +void (?create)?(long double _Imaginary*, long double); +void (?create)?(double*, double _Imaginary); +void (?create)?(double _Imaginary*, double); +void (?create)?(float*, float _Imaginary); +void (?create)?(float _Imaginary*, float); ++ +
Let SMax be the highest ranking signed integer type, and let +UMax be the highest ranking unsigned integer type. Then Cforall +would define
+ ++Creator(SMax, float); +Creator(SMax, float _Complex); +Creator(SMax, float _Imaginary); +Creator(UMax, float); +Creator(UMax, float _Complex); +Creator(UMax, float _Imaginary); ++ +
For every signed integer type T with rank greater than that of
+signed char
, Cforall would define
+Creator(S, T); ++
where S is the signed integer type with the next lowest rank.
+ +For every rank r greater than the rank of _Bool
,
+Cforall would define
+Creator(unsigned(r-1), unsigned(r)); ++ +
For every rank r such that signed(r)
exists,
+Cforall would define
+void (?create)?( signed(r)*, unsigned(r) ); ++ +
char
and _Bool
are interconvertible.
+void (?create)?(char*, _Bool); +void (?create)?(_Bool*, char); ++ +
If char
is equivalent to signed char
, the
+implementation would define
+Creator(char, signed char); +void (?create)?(char*, unsigned char); ++ +
Otherwise, the implementation would define
++Creator(char, unsigned char); +void (?create)?(char*, signed char); +void (?create)?(_Bool*, signed char); +void (?create)?(signed char*, _Bool); ++ +
Pointer types are implicitly interconvertible with pointers to void, +provided that the target type has all of the qualifiers of the source +type.
+ ++forall(dtype SourceType, + type QVPtr | void (?promote)?(QVPtr*, void*)) + void (?create)?(QVPtr*, SourceType*); ++ +
This conversion uses the constructor idiom, but note
+that the assertion parameter is a promotion even though the conversion
+itself is not a promotion. My intent is that the assertion parameter will
+be bound to a promotion that adds type qualifiers
+to a pointer type. A conversion from int*
to const
+void*
would bind SourceType
to int
,
+QVPtr
to const void*
, and the assertion parameter
+to a promotion from void*
to const void*
(which
+is a specialization of one of the polymorphic type qualifier promotions
+given above). Because of this composition of pointer conversions, I don't
+have to define conversions for every combination of type qualifiers on the
+target type. I do have to handle all combinations of qualifiers on the
+source type:
+forall(dtype SourceType, + type QVPtr | void (?promote)?(QVPtr*, const void*)) + void (?create)?(QVPtr*, const SourceType*); +forall(dtype SourceType, + type QVPtr | void (?promote)?(QVPtr*, volatile void*)) + void (?create)?(QVPtr*, volatile SourceType*); +forall(dtype SourceType, + type QVPtr | void (?promote)?(QVPtr*, restrict void*)) + void (?create)?(QVPtr*, restrict SourceType*); +forall(dtype SourceType, + type QVPtr | void (?promote)?(QVPtr*, const volatile void*)) + void (?create)?(QVPtr*, const volatile SourceType*); +forall(dtype SourceType, + type QVPtr | void (?promote)?(QVPtr*, const restrict void*)) + void (?create)?(QVPtr*, const restrict SourceType*); +forall(dtype SourceType, + type QVPtr | void (?promote)?(QVPtr*, volatile restrict void*)) + void (?create)?(QVPtr*, volatile restrict SourceType*); +forall(dtype SourceType, + type QVPtr | void (?promote)?(QVPtr*, const volatile restrict void*)) + void (?create)?(QVPtr*, const volatile restrict SourceType*); + +forall(type QTPtr, + dtype TargetType | void (?promote)?(QTPtr*, TargetType*) + void (?create)?(QTPtr*, void*); +forall(type QTPtr, + dtype TargetType | void (?promote)?(QTPtr*, const TargetType*) + void (?create)?(QTPtr*, const void*); +forall(type QTPtr, + dtype TargetType | void (?promote)?(QTPtr*, volatile TargetType*) + void (?create)?(QTPtr*, volatile void*); +forall(type QTPtr, + dtype TargetType | void (?promote)?(QTPtr*, restrict TargetType*) + void (?create)?(QTPtr*, restrict void*); +forall(type QTPtr, + dtype TargetType | void (?promote)?(QTPtr*, const volatile TargetType*) + void (?create)?(QTPtr*, const volatile void*); +forall(type QTPtr, + dtype TargetType | void (?promote)?(QTPtr*, const restrict TargetType*) + void (?create)?(QTPtr*, const restrict void*); +forall(type QTPtr, + dtype TargetType | void (?promote)?(QTPtr*, volatile restrict TargetType*) + void (?create)?(QTPtr*, volatile restrict void*); +forall(type QTPtr, + dtype TargetType | void (?promote)?(QTPtr*, const volatile restrict TargetType*) + void (?create)?(QTPtr*, const volatile restrict void*); ++ +
Function pointers are interconvertible.
++forall(ftype FT1, ftype FT2, type T | FT1* (?)?(T) ) FT2* (?)?(FT1*); ++ +
Data pointers including pointers to void
are
+interconvertible, regardless of type qualifiers.
+forall(dtype DT1, dtype DT2) DT2* (?)?(DT1*); +forall(dtype DT1, dtype DT2) const DT2* (?)?(DT1*); +forall(dtype DT1, dtype DT2) volatile DT2* (?)?(DT1*); +forall(dtype DT1, dtype DT2) const volatile DT2* (?)?(DT1*); + +forall(dtype DT1, dtype DT2) DT2* (?)?(const DT1*); +forall(dtype DT1, dtype DT2) const DT2* (?)?(const DT1*); +forall(dtype DT1, dtype DT2) volatile DT2* (?)?(const DT1*); +forall(dtype DT1, dtype DT2) const volatile DT2* (?)?(const DT1*); + +forall(dtype DT1, dtype DT2) DT2* (?)?(volatile DT*); +forall(dtype DT1, dtype DT2) const DT2* (?)?(volatile DT*); +forall(dtype DT1, dtype DT2) volatile DT2* (?)?(volatile DT*); +forall(dtype DT1, dtype DT2) const volatile DT2* (?)?(volatile DT*); + +forall(dtype DT1, dtype DT2) DT2* (?)?(const volatile DT*); +forall(dtype DT1, dtype DT2) const DT2* (?)?(const volatile DT*); +forall(dtype DT1, dtype DT2) volatile DT2* (?)?(const volatile DT*); +forall(dtype DT1, dtype DT2) const volatile DT2* (?)?(const volatile DT*); ++ +
Integers and pointers are interconvertible. For every integer type +I define
++forall(dtype DT, type T | I (?)?(T) ) DT* ?(?)(T); +forall(ftype FT, type T | I (?)?(T) ) FT* ?(?)(T); + +forall(dtype DT, type T | DT* (?)?(T) ) I (?)?(T); +forall(dtype DT, type T | DT* (?)?(T) ) I (?)?(T); ++ +
C99 has a few other "conversions" that don't fit into this proposal.
+Outside of some special circumstances (such as application of
+sizeof
),
int
or
+ unsigned int
values.I'd like to stop calling these "conversions". Perhaps they could be +handled by some verbiage in the semantics of "Primary Expressions".
+ +Cforall-as-is provides "specialization", which reduces the number of +type parameters or assertion parameters of a polymorphic object or +function. Specialization looks like a conversion -- it can happen +implicitly or as a result of a cast -- but would no longer be considered to +be a conversion.
+ +Since extended Cforall separates conversion from assignment, it can +simplify Cforall-as-is's set of assignment operators. Implicit conversions +can add type qualifiers to the target's type, and to the source's type in +the case of pointer assignment.
+ ++char ?=?(volatile char*, char); +char ?+=?(volatile char*, char); +// ... and similarly for the rest of the basic types and +// compound assignment operators. ++
+char c; +c = 'a'; // => ?=?( &c, 'a' ); + // => ?=?( (volatile char*)&c, 'a' ); ++ +
+// Assignment between data pointers, where the target has all of +// the qualifiers of the source. +forall(dtype DT) + DT* ?=?(DT* volatile restrict*, DT*); +forall(dtype DT) + const DT* ?=?(const DT* volatile restrict*, const DT*); +forall(dtype DT) + volatile DT* ?=?(volatile DT* volatile restrict*, volatile DT*); +forall(dtype DT) + const volatile DT* ?=?(const volatile DT* volatile restrict*, const volatile DT*); + +// Assignment to data pointers from voidpointers. +forall(dtype DT) DT* ?=?(DT* volatile restrict*, void*) +forall(dtype DT) + const DT* ?=?(const DT* volatile restrict*, const void*); +forall(dtype DT) + volatile DT* ?=?(volatile DT* volatile restrict*, volatile void*); +forall(dtype DT) + const volatile DT* ?=?(const volatile DT* volatile restrict*, const volatile void*); + +// Assignment to void pointers from data pointers. +forall(dtype DT) + void* ?=?(void* volatile restrict*, DT*); +forall(dtype DT) + const void* ?=?(const void* volatile restrict*, const DT*); +forall(dtype DT) + volatile void* ?=?(volatile void* volatile restrict*, volatile DT*); +forall(dtype DT) + const volatile void* ?=?(const volatile void* volatile restrict*, const volatile DT*); + +// Assignment from null pointers to other pointer types. +forall(dtype DT) + void* ?=?(void* volatile restrict*, forall(dtype DT2) const DT2*); +forall(dtype DT) + const void* ?=?(const void* volatile restrict*, forall(dtype DT2) const DT2*); +forall(dtype DT) + volatile void* ?=?(volatile void* volatile restrict*, forall(dtype DT2) const DT2*); +forall(dtype DT) + const volatile void* ?=?(const volatile void* volatile restrict*, forall(dtype DT2) const DT2*); + +// Function pointer assignment +forall(ftype FT) FT* ?=?(FT* volatile restrict*, FT*); +forall(ftype FT) FT* ?=?(FT* volatile restrict*, forall(ftype FT2) FT2*); ++ +
The difference, relative to Cforall-as-is, is that assignment operators
+come in one flavor (a pointer to a volatile value as the first operand)
+instead of two (a pointer to volatile in one case, a plain pointer in the
+other) or the four that restrict
would have led to.
However, to make this work, the type of default assignment
+functions must also change. A declaration of a type T
would
+implicitly declare
T ?=?(T volatile restrict*, T)
The constructor idiom is polymorphic in the
+object's type: an initial value of one particular type can initialize
+objects of many types. The constructor that promotes a Wazzit
+into a Thingum
is declared
+forall(type T | void (?promote)?(T*, Thingum) ) + void (?promote)?(T*, Wazzit); ++
("You can make a Wazzit
into a Thingum
and
+types higher in the hierarchy.")
It would also be possible to use a constructor idiom where the object's +type is fixed and the initial value's type is polymorphic:
+ ++forall(type T | void (?promote)?(Wazzit*, T) ) + void (?promote)?(Thingum*, T); ++
("You can make a Thingum
from a Wazzit
and
+types lower in the hierarchy.")
The "polymorphic value" idiom has the advantage that it is fairly
+obvious that the function is a constructor for type Thingum
.
+In the "polymorphic object" idiom, Thingum
is buried in the
+assertion parameter.
However, I chose the "polymorphic object" idiom because it matches C's
+semantics for signed-to-unsigned integer conversions. In the "polymorphic
+object" idiom, the natural way to write the polymorphic promoter from
+int
to larger types is
+
+forall(type T | void (?promote)?(T*, long) ) + void (?promote)?(T* tp, int i) { + long l = i; + *tp = (T)l; // calls the assertion parameter. + } ++ +
Now consider the case of a CPU with 16-bit int
s, where we
+need to convert an int
value -1
to a 32-bit
+unsigned long
. The assertion parameter will be bound to the
+monomorphic long
-to-unsigned long
promoter. The
+function body above converts the int
-1 to a long
+-1, and then uses the assertion parameter to convert the result to the
+correct unsigned long
value: 4,294,967,295.
In the "polymorphic value" idiom, the conversion would be done by
+calling the polymorphic promoter to unsigned long
from smaller
+types:
+forall(type T | void (?promote)?(unsigned*, T) ) + void (?promote)?(unsigned long* ulp, T t) { + unsigned u = t; // calls the assertion parameter. + *ulp = u; + } ++ +
This time the assertion parameter will be bound to the
+int
-to-unsigned
promoter. The function body uses
+the assertion parameter to convert the integer -1 to unsigned
+65,565, and then converts the result to the incorrect unsigned
+long
value 65,535.
Clearly the "polymorphic value" idiom would require the implementation +to do some unnatural, and probably implementation-dependent, bit mangling +to get the right answer. Of course, an implementation is allowed to +perform any unnatural acts it chooses. But programmers would have to +conform to the prevailing constructor idiom when writing their +constructors, and will want to write natural and portable code.
+ + + +