Does C99/C11 restrict type qualifier imply anythin

2020-07-18 07:20发布

问题:

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?

回答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.



回答2:

  • 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.


回答3:

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.