4

Suppose we have a function declaration for which we do not have access to its definition:

void f(int * restrict p, int * restrict q, int * restrict r);

Since we do not know how the pointers will be accessed, we cannot know if a call will trigger undefined behavior or not -- even if we are passing the same pointer, like the example at 6.7.3.1.10 explains:

The function parameter declarations:

void h(int n, int * restrict p, int * restrict q, int * restrict r)
{
    int i;
    for (i = 0; i < n; i++)
        p[i] = q[i] + r[i];
}

illustrate how an unmodified object can be aliased through two restricted pointers. In particular, if a and b are disjoint arrays, a call of the form h(100, a, b, b) has defined behavior, because array b is not modified within function h.

Therefore, is restrict superfluous in these cases, except as a hint/annotation for callers, unless we know something more about the function?


For instance, let's take sprintf (7.21.6.6) from the standard library:

Synopsis

#include <stdio.h>
int sprintf(char * restrict s,
     const char * restrict format, ...);

Description

The sprintf function is equivalent to fprintf, except that the output is written into an array (specified by the argument s) rather than to a stream. (...)

From the synopsis and the first sentence of the description, we know that s will be written to and that s is a restricted pointer. Therefore, can we already assume (without reading further) that a call like:

char s[4];
sprintf(s, "%s", s);

will trigger undefined behavior?

  • If yes, then: is the last sentence of sprintf's description superfluous (even if clarifying)?

    If copying takes place between objects that overlap, the behavior is undefined.

  • If not, then, the other way around: is the restrict qualifier superfluous since the description is the one that is actually letting us know what will be undefined behavior?

Acorn
  • 24,970
  • 5
  • 40
  • 69

3 Answers3

1

If yes, then: is the last sentence of sprintf's description superfluous (even if clarifying)?
If copying takes place between objects that overlap, the behavior is undefined.

int sprintf(char * restrict s,  const char * restrict format, ...);

The restrict on s means that reads and writes only depends on what sprintf() does. The following code does that, reading and writing data pointed by p1 as the char * restrict s argument. Read/write only happened due to direct sprintf() code and not a side effect.

char p[100] = "abc";
char *p1 = p;
char *p2 = p;
sprintf(p1, "<%s>", p2);

Yet when sprintf() accesses the data pointed to by p2 , there is no restrict. The “If copying takes place between objects that overlap, the behavior is undefined” applies to p2 to say p2's data must not change due to some side effect.


If not, then, the other way around: is the restrict qualifier superfluous since the description is the one that is actually letting us know what will be undefined behavior?

restrict here is for the compiler to implement the restrict access. We do not need to see it given the “If copying takes place...” spec.


Consider the simpler strcpy() which has the same “If copying takes place between objects that overlap, the behavior is undefined.”. It is redundant for us, the readers, here, as careful understanding of restrict (new in C99), would not need that.

char *strcpy(char * restrict s1, const char * restrict s2);

C89 (pre-restrict days) also has this wording for strcpy(), sprintf(), ... and so may be simply a left-over over-spec in C99 for strcpy().


The most challenging aspect of type * restrict p I find is that it refers to what will not happen to its data (p data will not change unexpectedly - only through p). Yet writing to p data is allowed mess up others - unless they have a restrict.

chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
  • 1
    One could write a function that behaved just like `memcpy`, and had the same `restrict` qualifiers, but still handled the overlap case without invoking Undefined Behavior. The trick would be to use a loop to check whether `src+i==dest` or `dest+i==src` for all indices within the range (defined behavior whether or not the pointers identify parts of the same object). If there is any `i` where one of the equalities holds, then `memmove(dest, (char*)dest+((char*)dest-(char*)src), n)` would have defined behavior. Since accesses would use addresses derived from `dest`, there would be no... – supercat May 18 '18 at 19:26
  • 1
    ...violation of the requirements for `restrict`. There are some cases where knowledge that a function's *definition* includes both `const` and `restrict` on a parameter may allow call-side optimizations that would not otherwise be possible, the presence of `restrict` in a prototype isn't binding upon a definition. – supercat May 18 '18 at 19:29
1
  • restrict was introduced in C99.
  • Since we do not know how the pointers will be accessed, we cannot know if a call will trigger undefined behavior
    Yes. But this is a question of trust. A function declaration is a contract, between the programmer who wrote the function definition and programmer that uses the function. Remember, once in C we would just write void f(); - here f is a function that takes an unspecified number of parameters. If you don't trust that programmer who wrote that function, no one will and don't use that functions. In C we are passing address of the first array element, so seeing a function declared like this, I would assume: the programmer who wrote that function gives some description on how these pointers are used or function f uses them as pointers to single element, not as arrays.
    ( In times like this I like to use C99 VLAs in function declaration to specify how long arrays my function expects: void f(int p[restrict 5], int q[restrict 10], int r[restrict 15]);. Such function declaration is exactly equal to yours, but you have some idea what memory can't overlap. )
  • char s[4]; sprintf(s, "%s", s); will trigger undefined behavior?
    Yes. Copying takes place between objects that overlap and restrict location is accessed by two pointers.
KamilCuk
  • 120,984
  • 8
  • 59
  • 111
  • Regarding: "*Copying will take place between objects that overlap.*": The "*will trigger UB?*" question is about whether you can know that *without* having read the last sentence of the description ("*If copying takes place between objects that overlap, the behavior is undefined*"). – Acorn May 17 '18 at 17:41
  • Regarding: "*... that first and second parameter will not overlap. They may overlap with other parameters.*": They may not overlap with any other pointer; not just with the other restricted pointers. See 6.7.3.1.7 for an example. – Acorn May 17 '18 at 17:45
  • As for C99: indeed, I should have added the tag -- done, thanks! – Acorn May 17 '18 at 17:57
  • Firstly, I don't see what `int p[restrict 5]` has to do with "C99 VLAs". It is indeed C99 syntax, but in your example it does not in any way involve VLAs. Secondly, if you really expect this size, adding `static` might make sense `int p[restrict static 5]` since in this form the compiler might even use that `5` for optimizations (as opposed to simply ignoring it). – AnT stands with Russia May 18 '18 at 20:18
0

Given a function signature like:

void copySomeInts(int * restrict dest, int * restrict src, int n);

someone who wanted the function to yield defined behavior in the case where the source and destination overlap [or even where they are equal] would need to expend some extra effort to do so. It would be possible, e.g.

void copySomeInts(int * restrict dest, int const * restrict src, int n)
{
  for (int i=0; i<n; i++)
  {
    if (dest+i == src)
    {
      int delta = src-dest;
      for (i=0; i<n; i++)
        dest[i] = dest[delta+i];
      return;
    }
    if (src+i == dest)
    {
      int delta = src-dest;
      for (i=n-1; i>=0; i--)
        dest[i] = src[delta+i];
      return;
    }
  }
  /* No overlap--safe to copy in normal fashion */
  for (int i=0; i<n; i++)
    dest[i] = src[i];
}

and thus there's no way a compiler generating code to call copySomeInts would be able to make any inferences about its behavior unless it could actually see the definition copySomeInts and not just the signature.

While the restrict qualfiers would not imply that the function couldn't handle the case where source and destination overlap, it they would suggest that such handling would likely be more complicated than would be necessary without the qualifier. This would in turn suggest that in unless there is of explicit documentation promising to handle that case, the function should not be expected to handle it in defined fashion.

Note that in the case where the source and destination overlap, no storage is actually addressed using the src pointer nor anything derived from it. If src+i==dest or dest+i==src, that would mean both src and dest identify elements of the same array, and thus src-dest would simply represent the difference in their indices. The const and restrict qualifiers on src mean that nothing which is accessed with a pointer derived from src can be modified in any way during the execution of the function, but that restriction only applies to things that are accessed with pointers derived from src. If nothing is actually accessed with such pointers, the restriction is vacuous.

supercat
  • 77,689
  • 9
  • 166
  • 211
  • @Deduplicator: What test? The `restrict` qualifier invites a compiler to assume that no storage which is accessed as the target of `dest` or a pointer derived from it will be accessed any other way, and likewise with `src`. I know of nothing in the Standard that would regard the comparison of a pointer's value with that of another pointer as any kind of access to the pointer's target, except in the narrow case of a pointer targeting itself. – supercat May 18 '18 at 21:21