Почему компилятор предполагает, что эти, казалось бы, равные указатели отличаются?
Похоже, что GCC с некоторой оптимизацией считает, что два указателя из разных единиц перевода никогда не могут быть одинаковыми, даже если они на самом деле одинаковы.
код:
main.c
#include <stdint.h>
#include <stdio.h>
int a __attribute__((section("test")));
extern int b;
void check(int cond) { puts(cond ? "TRUE" : "FALSE"); }
int main() {
int * p = &a + 1;
check(
(p == &b)
==
((uintptr_t)p == (uintptr_t)&b)
);
check(p == &b);
check((uintptr_t)p == (uintptr_t)&b);
return 0;
}
b.c
int b __attribute__((section("test")));
Если я скомпилирую его с -O0, он печатает
TRUE
TRUE
TRUE
Но с -O1
FALSE
FALSE
TRUE
Итак, p
и &b
на самом деле одно и то же значение, но компилятор оптимизировал их сравнение, полагая, что они никогда не могут быть равными.
Я не могу понять, какая оптимизация сделала это.
Это не похоже на строгий псевдоним, потому что указатели имеют один тип, а параметр -fstrict-aliasing
не делает этого.
Является ли это документированным поведением? Или это ошибка?
Ответы
Ответ 1
В коде есть три аспекта, которые приводят к общим проблемам:
-
Преобразование указателя в целое число определяется реализацией. Существует не гарантия преобразования двух указателей, чтобы все биты были идентичными.
-
uintptr_t
гарантированно конвертировать из указателя в тот же тип, а затем обратно без изменений (то есть сравнить его с исходным указателем). Но ничего больше. Целочисленные значения не гарантируют сопоставление равных. Например. могут быть неиспользуемые биты с произвольным значением. См. Стандарт 7.20.1.4.
-
И (вкратце) два указателя могут сравнивать только равные, если они указывают на тот же массив или прямо за ним (последняя запись плюс одна) или по крайней мере один - это нулевой указатель. Для любого другого созвездия они сравниваются неравномерно. Подробные сведения см. В стандарте 6.5.9p6.
Наконец, нет никакой гарантии, что переменные помещаются в память с помощью инструментальной цепочки (обычно это компоновщик для статических переменных, компилятор для автоматических переменных). Только массив или struct
(т.е. Составные типы) гарантируют упорядочение его элементов.
В вашем примере применяется 6.5.9p7. Он в основном обрабатывает указатель на объект без массива для сравнения, например, с первой записью массива размера 1
. Это означает, что не покрывает инкрементированный указатель прошлый объект, например &a + 1
. Релевантным является объект, на котором основан указатель. Это объект a
для указателя p
и b
для указателя &b
. Остальное можно найти в параграфе 6.
Ни одна из ваших переменных не является массивом (последняя часть параграфа 6), поэтому указатели не должны сравнивать равные, даже для &a + 1 == &b
. Последний "TRUE" может возникнуть из gcc, предполагая, что сравнение uintptr_t
возвращает true.
gcc, как известно, агрессивно оптимизирует, строго следуя стандарту. Другие компиляторы более консервативны, но это приводит к менее оптимизированному коду. Пожалуйста, не пытайтесь "решить" это, отключив оптимизацию или другие хаки, но исправьте ее с помощью четко определенного поведения. Это ошибка в коде.
Ответ 2
p == &b
представляет собой сравнение указателей и подчиняется следующим правилам из стандарта C (6.5.9. Операторы равенства, пункт 4):
Два указателя сравнивают одинаковые, если и только если оба являются нулевыми указателями, оба являются указателями на один и тот же объект (включая указатель на объект и подобъект в начале) или функцию, оба являются указателями на один из последних элементов один и тот же объект массива, или один - указатель на один конец конца одного объекта массива, а другой - указатель на начало другого объекта массива, который происходит сразу после первого объекта массива в адресном пространстве.
(uintptr_t)p == (uintptr_t)&b
является арифметическим сравнением и подчиняется следующим правилам (6.5.9 Операторы равенства, пункт 6):
Если оба операнда имеют арифметический тип, выполняются обычные арифметические преобразования. Значения сложных типов равны тогда и только тогда, когда обе их действительные части равны, а также их мнимые части равны. Любые два значения арифметических типов из доменов разных типов равны тогда и только тогда, когда результаты их преобразования в (сложный) тип результата, определяемый обычными арифметическими преобразованиями, равны.
Эти две отрывки требуют от реализации очень разных вещей. И понятно, что спецификация C не предъявляет требования к реализации, чтобы имитировать поведение прежнего вида сравнения в случаях, когда вызывается последний вид, и наоборот. Реализация требуется только для соблюдения этого правила (7.18.1.4. Целочисленные типы, способные удерживать указатели объектов в C99 или 7.20.1.4 на C11):
Тип [uintptr_t
] обозначает целочисленный тип без знака с тем свойством, что любой действительный указатель на void может быть преобразован в этот тип, а затем преобразован обратно в указатель на void, и результат будет сравниваться с исходным указателем.
(Приложение: приведенная выше цитата неприменима в этом случае, поскольку преобразование с int*
в uintptr_t
не включает void*
в качестве промежуточного шага. См. Хади отвечает за объяснение и цитату из этого. Тем не менее, рассматриваемое преобразование является реализацией, и для двух сравнений, которые вы пытаетесь, не требуется проявлять такое же поведение, что и основной вынос здесь.)
В качестве примера разницы рассмотрим два указателя, которые указывают один адрес двух разных адресных пространств. Сравнение их как указателей не должно возвращать true, но сравнение их с целыми числами без знака.
&a + 1
- целое число, добавленное к указателю, которое подчиняется следующим правилам (6.5.6 Аддитивные операторы, пункт 8):
Когда выражение, которое имеет целочисленный тип, добавляется или вычитается из указателя, result имеет тип операнда указателя. Если операнд указателя указывает на элемент объект массива и массив достаточно велик, результат указывает на смещение элемента от исходный элемент такой, что разность индексов результирующего и оригинального элементы массива равны целочисленному выражению. Другими словами, если выражение P указывает на i-й элемент объекта массива, выражения (P) + N (эквивалентно, N + (P)) и (P) -N (где N имеет значение n) указывает соответственно на я + n-й и i-n-й элементы объект массива, если они существуют. Более того, если выражение P указывает на последнее элемент объекта массива, выражение (P) +1 указывает один за последним элементом массив, и если выражение Q указывает один за последним элементом объекта массива, выражение (Q) -1 указывает на последний элемент объекта массива. Если оба указателя операнд и результат указывают на элементы одного и того же объекта массива или один за последним элемент объекта массива, оценка не должна приводить к переполнению; в противном случае поведение undefined. Если результат указывает один за последний элемент объекта массива, он не должен использоваться в качестве операнда унарного * оператора, который оценивается.
Я считаю, что эта выдержка показывает, что сложение (и вычитание) указателя определяется только для указателей внутри одного и того же объекта массива или за последним элементом. И поскольку (1) a
не является массивом и (2) a
и b
не являются членами одного и того же объекта массива, мне кажется, что ваша операция с указателем math вызывает поведение undefined и ваш компилятор использует его, чтобы предположить, что сравнение указателей возвращает false. Опять же, как указано в Hadi answer (и в отличие от того, что мой оригинальный ответ предполагается, что в данный момент), указатели на объекты без массива можно рассматривать как указатели на объекты массива длиной один, и, таким образом, добавление одного к вашему указателю на скаляр имеет право указывать на один конец конца массива.
Поэтому ваш случай, похоже, подпадает под последнюю часть первого отрывка, упомянутого в этом ответе, что делает ваше сравнение четким, чтобы оценивать значение true тогда и только тогда, когда две переменные связаны последовательно и в порядке возрастания. Является ли это верным для вашей программы, остается неуказанным стандартом и до реализации.
Ответ 3
Хотя один из ответов уже принят, принятый ответ (и все другие ответы на этот вопрос) критически ошибочны, поскольку я объясню и затем отвечу на вопрос. Я буду ссылаться на тот же стандарт C, а именно на n1570.
Начнем с &a + 1
. В отличие от того, что заявили @Theodoros и @Peter, это выражение определило поведение. Чтобы увидеть это, рассмотрим раздел 6.5.6, пункт 7 "Аддитивные операторы", который гласит:
Для целей этих операторов указатель на объект, который является не элемент массива ведет себя так же, как указатель на первый элемент массива длиной один с типом объекта как его тип элемента.
и пункт 8 (в частности, подчеркнутая часть):
Когда выражение, которое имеет целочисленный тип, добавляется или вычитается из указателя результат имеет тип операнда указателя. Если операнд указателя указывает на элемент объекта массива, а массив достаточно велика, результат указывает на смещение элемента от оригинальный элемент такой, что разность индексов результирующие и исходные элементы массива равны целочисленному выражению. Другими словами, если выражение P указывает на i-й элемент массива, выражения (P) + N (эквивалентно, N + (P)) и (P) -N (где N имеет значение n) указывают соответственно на я + n-й и i-n-ых элементов массива, если они существуют. Более того, if выражение P указывает на последний элемент объекта массива, выражение (P) +1 указывает один за последним элементом объекта массива, и если выражение Q указывает один за последним элементом массива объект, выражение (Q) -1 указывает на последний элемент массива объект. Если и операнд указателя, и результат указывают на элементы одного и того же объекта массива, или один за последним элементом массива объект, оценка не должна приводить к переполнению; в противном случае поведение undefined. Если результат указывает один за последним элементом объекта массива, он не должен использоваться как операнд унарного * оператор, который оценивается.
Выражение (uintptr_t)p == (uintptr_t)&b
имеет две части. Преобразование из указателя в uintptr_t
НЕ, определенное в разделе 7.20.1.4 (в отличие от того, что сказали @Olaf и @Theodoros):
Следующий тип обозначает целочисленный тип без знака с что любой действительный указатель на void может быть преобразован в этот тип, затем преобразуется обратно в указатель на void, и результат будет сравнивать равный исходному указателю:
uintptr_t
Важно признать, что это правило применяется только к действительным указателям на void
. Однако в этом случае мы имеем действительный указатель на int
. Соответствующий пункт можно найти в разделе 6.3.2.3, пункт 1:
Указатель на void может быть преобразован в или из указателя на любой объект тип. Указатель на любой тип объекта может быть преобразован в указатель на пустота и обратно; результат сравнивается с оригиналом указатель.
Это означает, что (uintptr_t)(void*)p
разрешено в соответствии с этим пунктом и 7.20.1.4. Но (uintptr_t)p
и (uintptr_t)&b
управляются разделом 6.3.2.3, пункт 6:
Любой тип указателя может быть преобразован в целочисленный тип. Кроме того, ранее указанный, результат определяется реализацией. Если результат не может быть представлен в целочисленном типе, поведение undefined. Результат не должен находиться в диапазоне значений любых целочисленный тип.
Обратите внимание, что uintptr_t
является целым типом, как указано в разделе 7.20.1.4, упомянутом выше, и поэтому это правило применяется.
Вторая часть (uintptr_t)p == (uintptr_t)&b
сравнивается для равенства. Как обсуждалось ранее, поскольку результат преобразования определяется реализацией, результат равенства также определяется реализацией. Это применяется независимо от того, равны ли сами указатели или нет.
Теперь я обсужу p == &b
. Третий пункт в ответе @Olaf неверен, и ответ @Theodoros является неполным относительно этого выражения. Раздел 6.5.9 "Операторы равенства", пункт 7:
Для целей этих операторов указатель на объект, который является не элемент массива ведет себя так же, как указатель на первый элемент массива длиной один с типом объекта как его тип элемента.
и пункт 6:
Два указателя сравнивают одинаковые, если и только если оба являются нулевыми указателями, оба являются указателями на один и тот же объект (включая указатель на объект и подобъектом в начале) или функцией, оба являются указателями на один за последним элементом одного и того же объекта массива, или один из них является указатель на один конец конца одного объекта массива, а другой - указатель на начало другого объекта массива, который происходит с немедленно следует за первым объектом массива в адресном пространстве.)
В отличие от того, что сказал @Olaf, сравнение указателей с использованием оператора ==
никогда не приводит к поведению undefined (что может произойти только при использовании реляционных операторов, таких как <=
в соответствии с разделом 6.5.8, параграф 5, который я здесь пропущу для краткости). Теперь, поскольку p
указывает на следующий int
относительно a
, он будет равен &b
, только если компоновщик разместил b
в этом месте в двоичном формате. В противном случае, неравноправны. Таким образом, это зависит от реализации (относительный порядок a
и b
не указывается стандартом). Поскольку объявления a
и b
используют расширение языка, а именно __attribute__((section("test")))
, относительные местоположения действительно зависят от реализации в J.5 и 3.4.2 (опущены для краткости).
Мы заключаем, что результаты check(p == &b)
и check((uintptr_t)p == (uintptr_t)&b)
зависят от реализации. Поэтому ответ зависит от того, какую версию компилятора вы используете. Я использую gcc 4.8 и компилируя параметры по умолчанию, кроме уровня оптимизации, вывод, который я получаю в обоих случаях -O0 и -O1, - это все TRUE.
Ответ 4
Согласно C11 6.5.9/6 и C11 6.5.9/7, тест p == &b
должен давать 1
, если a
и b
смежны в адресном пространстве.
В вашем примере показано, что GCC, похоже, не выполняет это требование Стандарта.
Обновление 26/Апр/2016: В моем первоначальном ответе содержались предложения по изменению кода для удаления других потенциальных источников UB и изоляции этого одного условия.
Однако с тех пор выяснилось, что проблемы, поднятые этим потоком, находятся в стадии рассмотрения - N2012.
Одна из их рекомендаций заключается в том, что p == &b
должен быть неуказан, и они признают, что GCC фактически не выполняет требования ISO C11.
Итак, у меня есть оставшийся текст из моего ответа, так как больше не нужно доказывать "ошибку компилятора", так как было установлено несоответствие (хотите ли вы назвать его ошибкой или нет).
Ответ 5
Перечитав вашу программу, я вижу, что вы (понятно) озадачены тем, что в оптимизированной версии
p == &b
false, а
(uintptr_t)p == (uintptr_t)&b;
истинно. Последняя строка указывает, что числовые значения действительно идентичны; как может p == &b
быть ложным?
Я должен признать, что понятия не имею. Я убежден, что это ошибка gcc.
После обсуждения с M.M я думаю, что могу сделать следующий случай, если преобразование в uintptr_t
проходит через промежуточный указатель void (вы должны включить это в свою программу и посмотреть, не изменит ли он что-либо):
Поскольку оба шага в цепочке преобразования int*
→ void*
→ uintptr_t
гарантированно обратимы, неравные int
указатели могут логически не приводить к равным значениям uintptr_t
. 1 (Те, которые равны значениям uintptr_t, должны были бы вернуться обратно к равным int указателям, изменив хотя бы один из них и тем самым нарушив правило преобразования, сохраняющее значение.) В коде (я не нацеливаю для равенства здесь, просто демонстрируя конверсии и сравнения):
int a,b, *ap=&a, *bp = &b;
assert(ap != bp);
void *avp = ap, *bvp bp;
uintptr_t ua = (uintptr_t)avp, ub = (uintptr_t)bvp;
// Now the following holds:
// if ap != bp then *necessarily* ua != ub.
// This is violated by the OP case (sans the void* step).
assert((int *)(void *)ua == (int*)(void*)ub);
1 Это предполагает, что uintptr_t не несет скрытую информацию в виде битов заполнения, которые не оцениваются в арифметическом сравнении, но, возможно, в преобразовании типа. Можно проверить это через CHAR_BIT, UINTPTR_MAX, sizeof (uintptr_t) и немного бит возиться. &mdash?
По той же причине можно предположить, что два значения uintptr_t сравниваются друг с другом, но преобразуются обратно к одному и тому же указателю (а именно, если в uintptr_t есть биты, которые не используются для хранения значения указателя, а конверсия не равна нулю). Но это противоположно проблеме ОП.