Подразумевает ли квалификатор типа ограничения C99/C11 что-либо для функций без определения?

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

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);

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

  • #P12# <блочная цитата> #P13#
  • Если нет, то наоборот: является ли квалификатор restrict излишним, поскольку именно описание дает нам знать, какое поведение будет undefined?


person Acorn    schedule 17.05.2018    source источник


Ответы (3)


Если да, то: является ли последнее предложение описания 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 не должны изменяться из-за какого-то побочного эффекта.


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

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.

person chux - Reinstate Monica    schedule 17.05.2018
comment
Можно написать функцию, которая ведет себя точно так же, как memcpy, и имеет те же квалификаторы restrict, но по-прежнему обрабатывает случай перекрытия, не вызывая Undefined Behavior. Хитрость заключается в том, чтобы использовать цикл для проверки того, является ли src+i==dest или dest+i==src для всех индексов в пределах диапазона (определяемое поведение, независимо от того, идентифицируют ли указатели части одного и того же объекта). Если есть какое-либо i, где выполняется одно из равенств, то memmove(dest, (char*)dest+((char*)dest-(char*)src), n) будет определять поведение. Поскольку доступ будет использовать адреса, производные от dest, не будет... - person supercat; 18.05.2018
comment
...нарушение требований к restrict. В некоторых случаях знание того, что определение функции включает в параметр как const, так и restrict, может позволить оптимизацию на стороне вызова, которая в противном случае была бы невозможна, присутствие restrict в прототипе не является обязательным. определение. - person supercat; 18.05.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); вызовет неопределенное поведение?
    Да. Копирование происходит между объектами, которые перекрываются и ограничивают доступ к расположению двумя указателями.
person KamilCuk    schedule 17.05.2018
comment
Относительно: Копирование будет происходить между перекрывающимися объектами.: Вопрос вызовет UB? вопрос в том, можете ли вы узнать об этом, не прочитав последнее предложение описания (Если копирование происходит между перекрывающимися объектами, поведение не определено). - person Acorn; 17.05.2018
comment
Относительно: ... этот первый и второй параметр не будут перекрываться. Они могут перекрываться с другими параметрами.: Они не могут перекрываться с любым другим указателем; не только с другими ограниченными указателями. См. пример в 6.7.3.1.7. - person Acorn; 17.05.2018
comment
Что касается C99: действительно, я должен был добавить тег - сделано, спасибо! - person Acorn; 17.05.2018
comment
Во-первых, я не понимаю, какое отношение int p[restrict 5] имеет к VLA C99. Это действительно синтаксис C99, но в вашем примере он никоим образом не связан с VLA. Во-вторых, если вы действительно ожидаете такой размер, добавление static может иметь смысл int p[restrict static 5], поскольку в этой форме компилятор может даже использовать этот 5 для оптимизации (а не просто игнорировать его). - person AnT; 18.05.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 и limited в src означают, что ничто, доступ к которому осуществляется с помощью указателя, полученного из src, не может быть каким-либо образом изменено во время выполнения функции, но это ограничение применяется только к вещам, доступ к которым осуществляется с помощью указателя, полученного из src. Если с помощью таких указателей фактически ни к чему не обращаются, ограничение бесполезно.

person supercat    schedule 18.05.2018
comment
@Deduplicator: Какой тест? Квалификатор restrict предлагает компилятору предположить, что никакое хранилище, доступ к которому осуществляется в качестве цели dest или производного от него указателя, не будет доступно каким-либо другим способом, и то же самое с src. Я не знаю ничего в Стандарте, что рассматривало бы сравнение значения указателя со значением другого указателя как любой вид доступа к цели указателя, за исключением узкого случая, когда указатель нацеливается сам на себя. - person supercat; 19.05.2018