Может ли сравнение сравнений несвязанных указателей оцениваться как истинное?

В разделе 6.5.9 раздела C в отношении операторов == и != указано следующее:

2 Должно быть выполнено одно из следующих условий:

  • оба операнда имеют арифметический тип;
  • оба операнда являются указателями на квалифицированные или неквалифицированные версии совместимых типов;
  • один операнд является указателем на тип объекта, а другой - указателем на квалифицированную или неквалифицированную версию void; или
  • один операнд является указателем, а другой - константой нулевого указателя.

...

6 Два указателя сравнивают одинаковые, если и только если оба являются нулевыми указателями, оба являются указателями на один и тот же объект (включая указатель на объект и подобъектом в начале) или функцией, оба являются указателями на один за последним элементом одного и того же объекта массива, или один - это указатель на один конец конца одного объекта массива, а другой - указатель на начало другого объекта массива, который происходит с немедленно следуйте за первым объектом массива в адресном пространстве. 109)

7 Для целей этих операторов указатель на объект, который не элемент массива ведет себя так же, как указатель на первый элемент массива длиной один с типом объекта как его тип элемента.

Сноска 109:

109) Два объекта могут быть смежными в памяти, поскольку они смежны элементы большего массива или смежные элементы структуры без между ними, или , потому что реализация выбрала место их так, хотя они не связаны. Если предыдущий неверный указатель операции (такие как доступ к границам внешнего массива), созданный undefinedповедение, последующие сравнения также приводят к поведению undefined.

Это, похоже, указывает, что вы можете сделать следующее:

int a;
int b;
printf("a precedes b: %d\n", (&a + 1) == &b);
printf("b precedes a: %d\n", (&b + 1) == &a);

Это должно быть законным, так как мы используем адрес один элемент за концом массива (который в этом случае является единственным объектом, обработанным как массив размером 1), без его разыменования. Что еще более важно, один из этих двух операторов потребуется для вывода 1, если одна переменная сразу же следует за другой в памяти.

Однако тестирование, похоже, не повлияло на это. Учитывая следующую тестовую программу:

#include <stdio.h>

struct s {
    int a;
    int b;
};

int main()
{
    int a;
    int b;
    int *x = &a;
    int *y = &b;

    printf("sizeof(int)=%zu\n", sizeof(int));
    printf("&a=%p\n", (void *)&a);
    printf("&b=%p\n", (void *)&b);
    printf("x=%p\n", (void *)x);
    printf("y=%p\n", (void *)y);

    printf("addr: a precedes b: %d\n", ((&a)+1) == &b);
    printf("addr: b precedes a: %d\n", &a == ((&b)+1));
    printf("pntr: a precedes b: %d\n", (x+1) == y);
    printf("pntr: b precedes a: %d\n", x == (y+1));

    printf("  x=%p,   &a=%p\n", (void *)(x), (void *)(&a));
    printf("y+1=%p, &b+1=%p\n", (void *)(y+1), (void *)(&b+1));

    struct s s1;
    x=&s1.a;
    y=&s1.b;
    printf("addr: s.a precedes s.b: %d\n", ((&s1.a)+1) == &s1.b);
    printf("pntr: s.a precedes s.b: %d\n", (x+1) == y);
    return 0;
}

Компилятор gcc 4.8.5, система CentOS 7.2 x64.

С -O0, я получаю следующий вывод:

sizeof(int)=4
&a=0x7ffe9498183c
&b=0x7ffe94981838
x=0x7ffe9498183c
y=0x7ffe94981838
addr: a precedes b: 0
addr: b precedes a: 0
pntr: a precedes b: 0
pntr: b precedes a: 1
  x=0x7ffe9498183c,   &a=0x7ffe9498183c
y+1=0x7ffe9498183c, &b+1=0x7ffe9498183c
addr: s.a precedes s.b: 1

Мы можем видеть здесь, что int составляет 4 байта и что адрес a равен 4 байтам за адресом b, а x содержит адрес a, а y имеет адрес b. Однако сравнение &a == ((&b)+1) оценивается как false, а сравнение (x+1) == y равно true. Я ожидал бы, что оба будут истинными, поскольку сравниваемые адреса выглядят идентичными.

С -O1 я получаю следующее:

sizeof(int)=4
&a=0x7ffca96e30ec
&b=0x7ffca96e30e8
x=0x7ffca96e30ec
y=0x7ffca96e30e8
addr: a precedes b: 0
addr: b precedes a: 0
pntr: a precedes b: 0
pntr: b precedes a: 0
  x=0x7ffca96e30ec,   &a=0x7ffca96e30ec
y+1=0x7ffca96e30ec, &b+1=0x7ffca96e30ec
addr: s.a precedes s.b: 1
pntr: s.a precedes s.b: 1

Теперь оба сравнения оцениваются как false, хотя (как и раньше) сравниваемый адрес выглядит одинаковым.

Это похоже на undefined поведение, но в зависимости от того, как я прочитал вышеприведенный отрывок, кажется, это должно быть разрешено.

Обратите внимание, что сравнение адресов соседних объектов одного и того же типа в struct выводит ожидаемый результат во всех случаях.

Я что-то неправильно понимаю о том, что разрешено (что означает UB), или это версия gcc-несоответствия в этом случае?

Ответ 1

Может ли сравнение сравнений несвязанных указателей оцениваться как true?

Да, , но...

int a;
int b;
printf("a precedes b: %d\n", (&a + 1) == &b);
printf("b precedes a: %d\n", (&b + 1) == &a);

По моей интерпретации стандарта C есть три возможности:

  • a сразу предшествует b
  • b сразу предшествует
  • ни a, ни b не предшествуют другому (между ними может быть пробел или другой объект).

Я играл с этим некоторое время назад и пришел к выводу, что GCC выполнял недопустимую оптимизацию для оператора == для указателей, делая его допустимым, даже когда адреса совпадают, поэтому я отправил отчет об ошибке:

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=63611

Эта ошибка была закрыта как дубликат другого отчета:

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=61502

Сторонники GCC, которые ответили на эти сообщения об ошибках, похоже, считают, что смежность двух объектов не должна быть последовательной и что сравнение их адресов может показать, что они смежны или нет, в рамках одного и того же запуска программы, Как вы можете видеть из моих комментариев ко второму билету Bugzilla, я категорически не согласен. На мой взгляд, без последовательного поведения оператора == стандартные требования к смежным объектам бессмысленны, и я думаю, что мы должны предположить, что эти слова не просто декоративны.

Здесь простая тестовая программа:

#include <stdio.h>
int main(void) {
    int x;
    int y;
    printf("&x = %p\n&y = %p\n", (void*)&x, (void*)&y);
    if (&y == &x + 1) {
        puts("y immediately follows x");
    }
    else if (&x == &y + 1) {
        puts("x immediately follows y");
    }
    else {
        puts("x and y are not adjacent");
    }
}

Когда я компилирую его с помощью GCC 6.2.0, печатные адреса x и y отличаются на 4 уровня на всех уровнях оптимизации, но я получаю y immediately follows x только при -O0; при -O1, -O2 и -O3 получаем x and y are not adjacent. Я считаю, что это неправильное поведение, но, по-видимому, оно не будет исправлено.

clang 3.8.1, на мой взгляд, ведет себя корректно, показывая x immediately follows y на всех уровнях оптимизации. Ранее у Клана была проблема с этим; Я сообщил об этом:

https://bugs.llvm.org/show_bug.cgi?id=21327

и он был исправлен.

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

(Обратите внимание, что реляционные операторы (<, <=, >, >=)) в указателях на несвязанные объекты имеют поведение undefined, но операторы равенства (==, !=) обычно необходимо вести себя последовательно.)

Ответ 2

int a;
int b;
printf("a precedes b: %d\n", (&a + 1) == &b);
printf("b precedes a: %d\n", (&b + 1) == &a);

- это совершенно четко определенный код, но, вероятно, скорее удача, чем суждение.

Вам разрешено принимать адрес скаляра и устанавливать указатель за этим адресом. Итак, &a + 1 действителен, но &a + 2 - нет. Вы также можете сравнить значение указателя того же типа со значением любого другого действительного указателя с помощью == и !=, хотя арифметика указателя действительна только в массивах.

Ваше утверждение о том, что адрес a и b сообщает вам о чем-либо о том, как они размещены в памяти, является блоком. Чтобы быть ясным, вы не можете "достигнуть" b по арифметике указателя по адресу a.

Что касается

struct s {
    int a;
    int b;
};

Стандарт гарантирует, что адрес struct совпадает с адресом a, но произвольное количество отступов разрешено вставлять между a и b. Опять же, вы не можете достигнуть адреса b любой арифметикой указателя по адресу a.

Ответ 3

Может ли сравнение сравнений несвязанных указателей оцениваться как true?

Да. C указывает, когда это правда.

Два указателя сравнивают одинаковые значения, если и только если... или один - указатель на один конец конца одного объекта массива, а другой - указатель на начало другого объекта массива, который происходит сразу после первого массива объект в адресном пространстве. C11dr §6.5.9 6

Чтобы быть ясным: смежные переменные в коде не обязательно должны быть смежными в памяти, но могут быть.


В приведенном ниже коде показано, что это возможно. Он использует дамп памяти int* в дополнение к обычным "%p" и (void*).

Однако код и вывод OP не отражают это. Учитывая "сравнивать равную, если и только если" часть вышеприведенной спецификации, IMO, OP компиляция не соответствует. Смежные в переменных памяти p,q того же типа, либо &p+1 == &q, либо &p == &q+1 должны быть истинными.

Нет мнения, если объекты различаются по типу - OP не запрашивает IAC.


void print_int_ptr(const char *prefix, int *p) {
  printf("%s %p", prefix, (void *) p);
  union {
    int *ip;
    unsigned char uc[sizeof (int*)];
  } u = {p};
  for (size_t i=0; i< sizeof u; i++) {
    printf(" %02X", u.uc[i]);
  }
  printf("\n");
}

int main(void) {
  int b = rand();
  int a = rand();
  printf("sizeof(int) = %zu\n", sizeof a);
  print_int_ptr("&a     =", &a);
  print_int_ptr("&a + 1 =", &a + 1);
  print_int_ptr("&b     =", &b);
  print_int_ptr("&b + 1 =", &b + 1);
  printf("&a + 1 == &b: %d\n", &a + 1 == &b);
  printf("&a == &b + 1: %d\n", &a == &b + 1);
  return a + b;
}

Выход

sizeof(int) = 4
&a     = 0x28cc28 28 CC 28 00
&a + 1 = 0x28cc2c 2C CC 28 00  <-- same bit pattern
&b     = 0x28cc2c 2C CC 28 00  <-- same bit pattern
&b + 1 = 0x28cc30 30 CC 28 00
&a + 1 == &b: 1                <-- compare equal
&a == &b + 1: 0

Ответ 4

Авторы Стандарта не пытались сделать это "доказательством на языке", и, как следствие, он несколько неоднозначен. Такая неоднозначность обычно не будет проблемой, когда составители компилятора делают добросовестную попытку отстаивать Принцип наименьшего удивления, поскольку существует явное неинтересное поведение, и любое другое поведение будет иметь поразительные последствия. С другой стороны, это означает, что авторы компиляторов больше заинтересованы в том, могут ли оправдаться оптимизации при любом чтении Стандарта, чем в том, совместимы ли они с существующим кодом, и могут найти интересные возможности для оправдания несовместимости.

Стандарт не требует, чтобы представления указателей несли какое-либо отношение к базовой физической архитектуре. Было бы абсолютно законно, чтобы система представляла каждый указатель как комбинацию дескриптора и смещения. Система, которая представляла бы указатели таким образом, могла бы свободно перемещать объекты, представленные таким образом вокруг, в физическом хранилище по мере ее соответствия. В такой системе первый байт объекта № 57 может следовать сразу после последнего байта объекта № 23 в один момент времени, но может быть в каком-то совершенно неродственном местоположении в какой-то другой момент. Я ничего не вижу в Стандарте, который запретил бы такую ​​реализацию сообщать "только что прошедший" указатель для объекта № 23 как равный указателю на объект № 57, когда два объекта оказались смежными, и как неравные, когда они не были связаны с быть.

Кроме того, в соответствии с правилом as-if реализация, которая была бы оправдана при перемещении объектов таким образом и имеющая причудливый оператор равенства, в результате была бы позволена иметь причудливый оператор равенства, независимо от того, физически или нет перемещаемые объекты в хранилище.

Если, однако, реализация указывает, как указатели хранятся в ОЗУ, и такое определение будет несовместимо с описанным выше поведением, это заставило бы реализацию реализовать оператор равенства в соответствии с этой спецификацией. Любой компилятор, который хочет иметь причудливый оператор равенства, должен воздерживаться от указания формата хранения указателя, который был бы несовместим с таким поведением.

Кроме того, Стандарт, по-видимому, подразумевает, что если код отмечает, что если два указателя с определенными значениями имеют одинаковое представление, они должны сравнивать равные. Чтение объекта с использованием символьного типа, а затем запись той же последовательности значений символьного типа в другой объект должна давать объект, эквивалентный оригиналу; такая эквивалентность является фундаментальной чертой языка. Если p является указателем "только что" одного объекта, а q является указателем на другой объект, а их представления копируются соответственно на p2 и q2, тогда p1 должно сравниваться с p и q2 до q. Если разнесенные представления типа символа p и q равны, это означает, что q2 был написан с той же последовательностью значений символьного типа, что и p1, что, в свою очередь, означало бы, что все четыре указателя должны быть равны.

Следовательно, хотя для компилятора было бы иметь причудливую семантику равенства для указателей, которые никогда не подвергаются коду, который мог бы наблюдать их представление на уровне байтов, такая поведенческая лицензия не будет распространяться на указатели, которые таким образом подвергаются. Если реализация определяет директиву или настройку, которая приглашает компиляторы для индивидуального сравнения произвольно сообщать о равных или неравных значениях при указании указателей на конец одного объекта и начале другого, размещение которых будет наблюдаться только при таком сравнении, реализация не будет иметь беспокоиться о соответствии в тех случаях, когда наблюдаются представления указателя. В противном случае даже в случае, если есть случаи, когда в соответствии с реализациями допускается иметь причудливую семантику сравнения, это не означает, что никакие реализации качества не должны делать этого, если только приглашение, если только указатель, расположенный за концом одного объекта, представление от указателя к началу следующего.