How do I declare a const pointer to non-const / mutable data in D?
Asked Answered
X

2

6

In D, how do I declare an either const or immutable pointer to non-const / mutable data in D?

The dlang website says you can't just declare it as const, as this makes both the pointer const and the data it points to is non-modifiable.

I've read earlier posts relating to this that suggest that it's simply impossible. If so, then that's a big hole in the language design. It should be possible to declare the pointer alone as non-modifiable, otherwise it's madness. Having the const propagate from pointer to also imply const data is possibly a useful default safety feature.

Xylia answered 6/7, 2016 at 4:35 Comment(2)
This is head const. Walter is fairly against it hence why it doesn't exist in D. The newsgroup has his reasoning.Slipstream
Possible duplicate of Right way to do "const pointer to non-const" in D?Bharal
M
5

You don't. In D, const, immutable, and shared are all transitive. So, once an outer part of the type is const (or immutable or shared), the whole type is. At one point, very early in D2, the language had both head const and tail const, but it was deemed too complicated to be worth it, and it was removed (back in version 2.015 IIRC). So, now const, immutable, and shared are fully transitive, and it's been that way for years.

You can declare stuff like

const(int)* p;

so that the inner portion of the type is const, but there is no way to indicate that the outer portion is const without making everything inside it const as well.

So, yes, what you're trying to do is impossible in D, which is perhaps less flexible than might be ideal, but head const is also hands down the least useful form of const. So, while it might be a loss, as far as I can tell, it really isn't a big one. And allowing head const would really complicate things - especially when immutable and shared came into play. So, the current system is much simpler without losing much power, making it arguably a very good tradeoff.

If you really wanted something like head const, you could always create a wrapper type that disallowed assignment, but that's the closest that you're going to get.

Marya answered 6/7, 2016 at 7:43 Comment(1)
Addition: Note that in D it's easy to implement HeadConst within user codeFunctionary
C
0

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.

Chipboard answered 26/6, 2021 at 19:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.