The problem with const proposals
Oskar Linde
oskar.lindeREM at OVEgmail.com
Sun Dec 9 07:24:09 PST 2007
Walter Bright wrote:
> So, what are our axioms for const/invariant?
>
> 1) Const/invariant are transitive.
> 2) Any type T can be wrapped inside a struct S, and that S can be made
> to behave as a typedef for type T.
> 3) The contents of invariant types, once constructed, can never change.
> 4) The contents of const types, once constructed, can never be changed
> through those const contents.
> 5) For any type T, we can construct a const(T) or an invariant(T)
> without knowing anything about T.
Those "axioms" may a bit too specific and already locks the design into
something sub-optimal. I think it would be better to specify design
requirements that better show what issues need to be handled.
My orthogonal const proposal would handle most of those without problem,
and does away with the overlapping nature of const/invariant. It
certainly handles 1,2,3,4. 5 is fulfilled subject to my interpretation.
They key issue is separating what is constant from the access mode
associated with pointers, references and other views.
The old "invariant" just means a view of constant data. You know the
data is constant and are therefore able to make use of that in
optimizations and other cases.
Separating those two aspects makes everything fall out simple and
consistent:
== "Constness"
The keyword const would mean "constant" and the meaning is the
established one that the compiler may rely on. Constant refers to the
actual data (binary representation). Constant variables may not be
changed after initialization.
= Plain old data
A constant (non-manifest versions):
const int a = 5;
const int b = runtimeFunction(); // Ok
const int c = rand(); // Ok
The constness of those are a part of their type, so typeof(a) == const(int).
A struct with constant members becomes non-assignable (but copyable):
struct S {
const int a;
int b;
}
The const member has to be initialized at struct initialization.
A a = S(1,2); // Ok
A b = a; // Ok
a = b; // Error: assignment to a constant
a.b = b.b; // Ok
const S c = S(1,2); // Ok
c.b = b.b; // Error
= Pointers/Arrays
A pointer and array slice to/of constant data:
const(int) *a;
const(int) a[];
A constant pointer to an int:
int x;
const int y;
const int* a = &x; // Ok
*x = 5; // Ok
a++; // Error: modifying a constant
const int* b = &y; // Error: incompatible types
*b = 5; // Ok
const(int) *c = &y;
c++; // Ok
(*c)++; // Error: modifying a constant
const const(int) *d = &y; // A constant pointer to a constant int
d++; // Error: modifying a constant
= Function arguments
void foo(ref const(int) x) {
x = 5; // Error: modifying a constant
}
The constness becomes part of the type and const(int)* is not
convertible to int*, nor vice versa. Apart from classes, that I will
return to later, that is about all there is to say about const.
== Access modes: Read only views
While const above refers to the modifiability of the actual data, the
read-only part refers to the access attributes of a reference. I will
use the keyword "in" to mean read-only below, but please substitute with
your favorite keyword. (Such as "read", "readonly", "protected" "view", etc)
int x;
const int y;
in int * a = &x; // Ok
*a = 5; // Error: No write access
in int * b = &y; // Ok
*b = 5; // Error: No write access
in const(int) * c = &x; // Error: x is not constant
in const(int) * d = &y; // Ok
typeof(&x) == int *
typeof(&y) == in const(int) *
so "in const(int) *" is implicitly convertible to "in int *", but not
the other way around.
Note that the read-only access modifier is implicit in pointers to
constant data.
Applying the read-only property to a type means all members of the type
are read-only:
Struct S {
int *a;
int b;
}
in S s;
s.b = 5; // the read-only part doesn't refer to the actual data,
// just the access through references
s.a = &x; // Ok;
*s.a = 5; // Error: No write access though S
An array slice behaves identical to the struct S above.
in int[] t;
t = &x[0..1]; // Ok
t[0] = 5; // Error: No write access though S
There is a wish to make the read-only property transitive, which means
that applying the read-only property to a type means that all types
referred to by the type also gets the read-only property:
int *m = &x; // Ok
in int **b = &m; // Ok
**b = 5; // Error, since typeof(*b) == in int *m
Everything is nice and consistent so far. The remaining parts are A)
member functions and B) classes.
== Member functions.
Member functions (methods) are called though a reference. If that
reference only has read-only access, only such member functions are
allowed to be called. No changes here. Transitivity of the read-only
propert will have to be handled manually. Something like:
struct Array(T) {
T[] arr;
accessof!(this) T get()(int i) { return arr[i]; }
static if (!is(T == const)) {
void set(int i, ref T v) { arr[i] = v; }
}
}
Array!(int);
Array!(int *);
Array!(const int); // <- This is the "invariant" case
== Classes
Since classes don't have a value type, it is not possible to define
constant classes. I have two views on this. You could come up with a
syntax that makes it possible to declare a type that is a reference to a
constant class, but it seems to me that most classes that are meant to
be constant have to be designed specifically for this purpose, so I
propose making the constant a part of the class declaration:
const class String {
this(char[] data) {...}
...
}
For classes that support both a modifiable and a constant mode, and the
constant mode is important enough to let the compiler know about, one
could define:
const class ConstString : MutableString {
// any non-read-only method of the superclass
// is blocked
...
}
Classes are special beasts that have other was to do access control (a
read only view interface for instance).
To end, I'd be very interested in hearing what the issues with this
design are and what unspoken design requirements this fails to fulfill.
--
Oskar
More information about the Digitalmars-d
mailing list