Ограничивает ли C99 / C11 ограничитель типа что-либо для функций без определения? - PullRequest
0 голосов
/ 17 мая 2018

Предположим, у нас есть объявление функции, для которого у нас нет доступа к ее определению:

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

Поскольку мы не знаем, как будут обращаться указатели, мы не можем знать, будет ли вызов вызывать неопределенныйповедение или нет - даже если мы передаем один и тот же указатель, как показано в примере на 6.7.3.1.10:

Объявления параметров функции:

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];
}

показывают, какнемодифицированный объект может быть наложен через два ограниченных указателя.В частности, если a и b являются непересекающимися массивами, вызов вида h(100, a, b, b) имеет определенное поведение, поскольку массив b не изменяется в функции h.

Таким образом, restrict является излишним в этих случаях, за исключением случаев, когда в качестве подсказки / аннотации для вызывающих абонентов мы разве что-то знаем о функции?


Например, давайте возьмем sprintf (7.21.6.6) из стандартной библиотеки:

Синопсис

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

Описание

Функция sprintfэквивалентно fprintf, за исключением того, что вывод записывается в массив (заданный аргументом s), а не в поток.(...)

Из краткого обзора и первого предложения описания мы знаем, что в s будет записано, а s является ограниченным указателем.Следовательно, можем ли мы уже предполагать (не читая дальше), что вызов типа:

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

вызовет неопределенное поведение?

  • Если да, то: последнийпредложение описания sprintf излишне (даже если поясняет)?

    Если копирование происходит между объектами, которые перекрываются, поведение не определено.

  • Если нет, то, наоборот: является ли излишним квалификатор restrict, поскольку описание действительно дает нам знать, что будет неопределенным поведением?

Ответы [ 3 ]

0 голосов
/ 17 мая 2018
  • restrict был введен в C99.
  • Since we do not know how the pointers will be accessed, we cannot know if a call will trigger undefined behavior
    Да. Но это вопрос доверия. Объявление функции - это контракт между программистом, который написал определение функции, и программистом, который использует функцию. Помните, что однажды в C мы просто напишем void f(); - здесь f - функция, которая принимает неопределенное количество параметров. Если вы не доверяете тому программисту, который написал эту функцию, никто не будет и не будет использовать эти функции. В C мы передаем адрес первого элемента массива, поэтому, видя функцию, объявленную так, я бы предположил: программист, написавший эту функцию, дает некоторое описание того, как используются эти указатели, или функция f использует их как указатели на один элемент, не как массивы.
    (В такие времена мне нравится использовать C99 VLA в объявлении функции, чтобы указать, как долго ожидают массивы моя функция: void f(int p[restrict 5], int q[restrict 10], int r[restrict 15]);. Такое объявление функции в точности совпадает с вашим, но у вас есть представление о том, что память не может перекрываться.)
  • char s[4]; sprintf(s, "%s", s); вызовет неопределенное поведение?
    Да. Копирование происходит между объектами, которые перекрывают и ограничивают местоположение, доступ к которому осуществляется двумя указателями.
0 голосов
/ 18 мая 2018

С учетом сигнатуры функции, например:

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

Кто-то, кто хотел, чтобы функция вырабатывала определенное поведение в случае, когда источник и пункт назначения перекрываются [или даже там, где они равны], должен будет приложить дополнительные усилия для этого. Это было бы возможно, например,

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];
}

и, таким образом, компилятор, генерирующий код для вызова copySomeInts, не сможет сделать какие-либо выводы о его поведении, если он не увидит определение copySomeInts, а не только сигнатуру.

Хотя квалификаторы restrict не означают, что функция не может обработать случай, когда источник и пункт назначения перекрываются, они бы предположили, что такая обработка, вероятно, будет более сложной, чем это было бы необходимо без квалификатора. Это, в свою очередь, предполагает, что, если только нет явной документации, обещающей обработать этот случай, не следует ожидать, что функция будет обрабатывать ее определенным образом.

Обратите внимание, что в случае, когда источник и место назначения перекрываются, фактически ни одно хранилище не адресуется с использованием указателя src или чего-либо полученного из него. Если src+i==dest или dest+i==src, это будет означать, что src и dest идентифицируют элементы одного и того же массива, и, таким образом, src-dest будет просто представлять разницу в их индексах. Спецификаторы const и restrict для src означают, что ничто, к которому осуществляется доступ с помощью указателя, полученного из src, не может быть изменено любым способом во время выполнения функции, но это ограничение применяется только к вещам, доступ к которым осуществляется с помощью указателей, полученных из src. Если с такими указателями на самом деле ничего не получается, ограничение является пустым.

0 голосов
/ 17 мая 2018

Если да, то: является ли последнее предложение описания sprintf излишним (даже если поясняющим)?
Если копирование происходит между перекрывающимися объектами, поведение не определено.

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

restrict на s означает, что чтение и запись зависит только от того, что делает sprintf().Следующий код делает это, читая и записывая данные, указанные p1 в качестве аргумента char * restrict s.Чтение / запись происходило только из-за прямого sprintf() кода, а не из-за побочного эффекта.

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

Тем не менее, когда sprintf() обращается к данным, на которые указывает p2, restrict отсутствует.«Если копирование происходит между объектами, которые перекрываются, поведение не определено» применяется к p2, чтобы сказать, что данные p2 не должны изменяться из-за какого-либо побочного эффекта.


Если нет, тогда, наоборот: является ли ограничивающий квалификатор лишним, поскольку описание является тем, которое фактически дает нам знать, что будет неопределенным поведением?

restrict здесь для компиляторареализовать доступ restrict.Нам не нужно это видеть, учитывая спецификацию «Если копирование происходит ...».


Рассмотрим более простой strcpy(), который имеет то же самое «Если копирование происходит между объектами, которые перекрываются, топоведение не определено ».Это избыточно для нас, читатели, здесь , так как тщательное понимание restrict (новинка в C99) не нуждается в этом.

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

C89 (до restrict дней) также имеет эту формулировку для strcpy(), sprintf(), ..., и поэтому может быть просто избыточной спецификацией в C99 для strcpy().


Самым сложным аспектом type * restrict p, который я нахожу, является то, что он относится к тому, что не случится с его данными (p данные не изменятся неожиданно - только через p).Тем не менее, запись в p данные допускает путаницу с другими - если у них нет restrict.

...