'restrict' keyword - Why is it allowed to assign from a outer restricted variable to an inner restricted variable?
Asked Answered
O

2

16

First some references. The C99 Standard says this about restrict in section 6.7.3:

An object that is accessed through a restrict-qualified pointer has a special association with that pointer. This association, defined in 6.7.3.1 below, requires that all accesses to that object use, directly or indirectly, the value of that particular pointer.117) The intended use of the restrict qualifier (like the register storage class) is to promote optimization, and deleting all instances of the qualifier from all preprocessing translation units composing a conforming program does not change its meaning (i.e., observable behavior).

And then (§6.7.3.1 "Formal definition of restrict"):

Let D be a declaration of an ordinary identifier that provides a means of designating an object P as a restrict-qualified pointer to type T.

If D appears inside a block and does not have storage class extern, let B denote the block. If D appears in the list of parameter declarations of a function definition, let B denote the associated block. Otherwise, let B denote the block of main (or the block of whatever function is called at program startup in a freestanding environment).

In what follows, a pointer expression E is said to be based on object P if (at some sequence point in the execution of B prior to the evaluation of E) modifying P to point to a copy of the array object into which it formerly pointed would change the value of E.119) Note that ''based'' is defined only for expressions with pointer types.

During each execution of B, let L be any lvalue that has &L based on P. If L is used to access the value of the object X that it designates, and X is also modified (by any means), then the following requirements apply: T shall not be const-qualified. Every other lvalue used to access the value of X shall also have its address based on P. Every access that modifies X shall be considered also to modify P, for the purposes of this subclause. If P is assigned the value of a pointer expression E that is based on another restricted pointer object P2, associated with block B2, then either the execution of B2 shall begin before the execution of B, or the execution of B2 shall end prior to the assignment. If these requirements are not met, then the behavior is undefined.

As some have pointed out, this illustrates the rules (Example 4 from the standard):

{
    int * restrict p1;
    int * restrict q1;

    p1 = q1; //  undefined behavior

    {
        int * restrict p2 = p1; //  valid
        int * restrict q2 = q1; //  valid
        p1 = q2; //  undefined behavior
        p2 = q2; //  undefined behavior
    }
}

Now, my first question is this: why is it okay to assign from an outer restricted pointer to an inner one?

My understanding is that nothing forbids this, which has clear aliasing:

int * restricted x = /* ... */ ;

{
    int * restricted y = x;
    *x = 3;
    printf("%d\n", *y); // 3
    *y = 4;
    printf("%d\n", *x); // 4
}

Of course, the set of aliases is restricted to the two pointers.

Hence my second question: what is the difference assigning from outer to inner (allowed), but not from inner to outer (forbidden, e.g. p1 = q1; in the first example above)?

Ontina answered 26/3, 2015 at 10:32 Comment(7)
Please change it so that it actually compiles.Goodhumored
Wasn't the only place where you can declare a pointer as restrict in the parameter list to a function?Lancinate
@FUZxxl Nope, the standard doesn't say so, and it works perfectly well with all warnings and strict standard compliance enabled.Ontina
The semantics of restrict would have been more understandable if it described what the compiler was allowed to do than what the programmer is forbidden from doing, but I think the basic idea is that restrict allows a compiler to behave as though any read through a restrict pointer is performed anywhere in the execution sequence between the assignment of the pointer and the point the logical read occurs, and writes are allowed to behave as though they occur any time within the lifetime of the pointer beyond where the logical write occurs, save that reads and writes can't occur...Ceraceous
...during the lifetime of a derived pointer. Given *p = 2; restrict q=p; doSomething(q); p=p2;, a compiler would be allowed to defer the write to *p until up to the point where p gets reassigned, which would make the write unsequenced with regard to anything done via q.Ceraceous
If you have two functions void foo(int *restrict a, int *restrict b) and void bar(int *restrict c, int *restrict d), presumably you agree that bar should be able to call foo(c, d). You're also allowed to "inline" foo "by hand" by placing its implementation in a nested block.Protege
restrict is useful in a local variable like double* restrict foo = malloc(42 * sizeof(double));Purpleness
C
2

I think the rules are designed to satisfy two objectives:

  1. Allow creation of a temporary pointer similar to what would naturally be created when an argument is passed to a function call, without requiring that code using the pointer be moved into a physically separate function.

  2. Ensure that a graph showing pointer derivation will be free of cycles (meaning that if pointer x is derived from y or anything that is directly or indirectly derived from y, y cannot be derived from x nor anything which is directly or indirectly derived from x). While the rules may be tighter than would be absolutely necessary to achieve #2, nearly all of the useful cases that would satisfy this second requirement also satisfy the rules as written, and compilers would have difficulty getting much benefit from restrict in the cases that don't.

Unfortunately, the authors of the rules don't seem to have made any particular effort to consider all possible corner cases and ensure that the rules in the Standard could be sensibly applied to all of them. There are two usage cases where the semantics of restrict are clear and sensible:

  1. A function receives a restrict-qualified pointer, in which case the qualifier should affect the pointer value passed to the function, independent of anything else that might be stored in the argument.

  2. The definition of an automatic object with a restrict qualifier includes an initialization expression, in which case the qualifier should affect the pointer value used for the initialization, independent of anything else that might be stored in the object.

The semantics of having restrict "guard" anything other than an address stored into a pointer via one of the two means above are rather murky at best, even if one tries to add scope rules like those in the Standard. Adding restrict qualifiers in other cases would be more likely to break something as to have a useful effect, even though the most likely outcome would be that the qualifiers would have no effect whatsoever.

Ceraceous answered 17/5, 2017 at 20:46 Comment(0)
G
-3

None of these are undefined behaviour. You can assign between restrict pointers all you like. Undefined behaviour may occur if you assign to the objects pointed to by restrict pointers in the wrong way.

And in your second example, the pointer y is derived from x, so assigning first to *x, then to the same variable as *y, is fine as well.

Instead of reading the legalese, have you actually thought about what "restrict" is supposed to achieve?

Goodhumored answered 26/3, 2015 at 11:0 Comment(3)
Well, the standard contradicts you without doubt. And yes I have thought about it; and I'm asking the question since my intuitive understand precisely doesn't mesh with what the standard says. I'm making the assumption the standard wasn't written by complete idiots, and so there must be a reason for its quirks. Also, this is a comment; not an answer.Ontina
Could you point out where I'm erring instead of dishing out ad hominems? Also consider that it's not only my reading but also that of other stackoverflow users on the question I've linked and here, with no one to contradict (of course, that doesn't prove anything).Ontina
@Norswap: Many corner cases are quirky because the authors of the Standard made no effective effort to handle them sensibly--presumably because they didn't happen to consider them. Because the Standard allows implementations to behave sensibly in cases where they're not required to, the authors placed higher priority in avoiding requiring compilers to handle corner cases badly than in requiring them to handle corner cases well.Ceraceous

© 2022 - 2024 — McMap. All rights reserved.