Unfortuately, it seems like D doesn't support it directly although, I think, there are certainly more fair use cases for a constant pointer to non-constant data than there are fair use cases for a mutable pointer value (non-constant pointers are needless mostly).
But it's not over!
D is not too strict with its "tail-const" philosophy, reasonably, otherwise common mechanism like call-by-reference wouldn't work. D does have "head-const" pointers but they are obfuscatedly called "references" (I'm not talking about reference type variables that are used for class-type variables).
As I understood, D officially introduced references due to safety reasons and therefore the places where references can be defined are limited to places where interfacing takes place (function paramaters and foreach variables). You cannot freely define a reference in your code.
It's best to explain references in D to be constant pointers which are automatically indirected whenever you use them. The big advantage over mutable pointers is, you can't reassign them with another memory region and they are much less likely or actually never null
. Whenever possible, use a reference and avoid pointers. The only problem: D's ref
is more constrained than C++, so how to make sure, you're propagating the actual reference without moving the object onto the stack?
The actual purpose of references in D is to represent input+output parameters (parameters that are updated in a function or foreach loop or by the caller when used as return value) as opposed to input- or output-only parameters.
Note, there is also an inout
keyword but it has a different meaning (it's a type-qualifier-template standing for const
, immutable
and non-const).
For output-only parameters you use out
. For input-only parameters you can use the newer in
which permits lvalue and rvalue arguments according to the documentation.
If you want to assign a pointer to a reference, use pointer indirection: ref int x = *pointer
. If you'd like to assign a reference to a mutable pointer then use the address: int* x = &reference
.
Now we're getting to how to circumvent D's restricted pointer philosophy:
int x = 3, k = 3;
ref int getInt(int n = 3 /*does nothing*/) {
return .x; // use '.globalVar' for readability
}
int* getIntPointer() {
return &.x;
}
void updateInt(ref int x) {
x += 1;
}
void main()
{
import std.stdio : writeln;
int i = getInt();
updateInt(i);
writeln(.x);
writeln(i);
(ref int j = getInt(k)) { // using 'k' is no problem
updateInt(j);
writeln(.x);
writeln(j);
}(); // this is a lambda expression
(ref int j = *getIntPointer()) {
updateInt(j);
writeln(.x);
writeln(j);
}();
S s = S();
s.update(5);
writeln(.x);
writeln(s.y);
}
struct S {
static int y = 5;
static ref int getSetInt(int n) {
return .x = n;
}
void update(int k) {
(ref int j = S.y) { // does not work with this.y
.updateInt(j);
}();
(ref int j = getSetInt(k)) { // does not work with instance methods
.updateInt(j);
}();
}
}
Notably, you cannot use instance members or methods from struct/class objects in the default argument value of the lambda expression. I suppose because lambda expressions are internally treated as static. That's why you cannot use this
in the default value (except for static class/struct methods).
There is obviously a drawback in this approach. The code block – which looks like a let-block in a functional programming language – is actually a lambda expression and a return
statement in the code block will only return control from the lambda expression, not from the function around it which is confusing.
It turns out returning your reference from a function is more tricky:
return *(ref int var = value) { return &var; }(); // cannot return references, only pointers
return *((ref int var = value) => &var)(); // can't avoid the inner pair of parentheses
Of course you shouldn't do this unless you know exactly that value
is valid in the caller. The good thing, D requires you to be conscious about what you do. You cannot just pass a returned reference from the lambda to the return statement or to a pointer variable because the returned reference is an rvalue.
You also can define multiple references of course:
(ref int var1 = val1, ref int var2 = val2, ...) {
...
}();
The argument, that constant pointers to non-constant data would be complicated, does not work out. D uses it all the time just in (too) restricted syntactical way which does not prevent you from using it how you need it (luckily). D's philosophy makes code more verbose here but at least a little more difficult for programmers to return dangling references so that it's unlikely to happen in accident.