Комментарии 105
И тут обнаруживается кое-что интересное. «Идентично» — то есть, тождественно, взаимозаменяемо при любых обстоятельствах. И даже при UB?
Значит в стандарте противоречие?
По идее, тогда компилятор должен выбирать специфицированный (одинаковое поведение), вместо неспецифицированного (UB) варианта.
Это как если бы область видимости методов ограничивалась при использовании private. Если его изменить на public, то работа корректного кода может измениться.
По-моему, компиляторы слишком увлеклись оптимизацией при обнаружении UB — в случае массива гораздо полезнее было бы предупреждение.
компиляторы слишком увлеклись оптимизацией при обнаружении UB — в случае массива гораздо полезнее было бы предупреждение
Я тоже так сначала подумал.
Но это ж C. Не хочется UB, нужно использовать что-то более высокоуровневое.
А вот компилятор, раз уж такой умный, мог бы сделать Си чуть получше.
Вопрос в том, почему, если компилятор видит, что код явно с ошибкой, то он его выкидывает, а не сообщение выводит?
В gcc можно включить такие предупреждения с помощью -Warray-bounds и заставить его останавливать сборку при наличии любых предупреждений с помощью -Werrors
Простите, я не разбираюсь в си.
У меня есть предположение, что концепция ub была введена, чтобы компилятор работал быстрее и не тратил ресурсы на поиск неадекватного кода. Насколько адекватно моё предположение?
И связанный вопрос: замедляется ли (или замедлялась ли в прошлом) сборка проекта при этих флагах?
А раньше компиляторы и не умели даже в простых случаях выход за границы массива детектить, это только недавно появилось.
А сейчас Разработчик Вселенной, наверное, сидит за терминалом, в ступоре от происходящего
Это он еще не увидел надпись «ТВОЙ БЫДЛОКОД НАС ОГОРЧАЕТ»
p.s. а пост очень интересный, спасибо!
А теперь давайте хоть однажды в жизни еще раз почитаем стандарт С.
Да, в разделе, посвященном массивам, он честно предупреждает, что доступ к элементам за границами массива – это UB.
А можно подробнее, где именно это в стандарте в разделе про массивы? Я там этого не нашел, но…
Но, как знает каждый, кто когда-либо изучал С, доступ к элементам массива возможен через указатели, или, языком стандарта ISO/IEC 9899 «определение оператора взятия индекса [] таково: E1[E2] эквивалентно (*((E1)+(E2))).» («The definition of the subscript operator [] is that E1[E2] is identical to (*((E1)+(E2))»).
В разделе, описывающем операторы + и — (при сложении с указателем), нашел следующее (6.5.6 Additive operators, п. 8):
When an expression that has integer type is added to or subtracted from a pointer, the result has the type of the pointer operand. If the pointer operand points to an element of an array object, and the array is large enough, the result points to an element offset from the original element such that the difference of the subscripts of the resulting and original array elements equals the integer expression. In other words, if the expression P points to the i-th element of an array object, the expressions (P)+N (equivalently, N+(P)) and (P)-N (where N has the value n) point to, respectively, the i+n-th and i−n-th elements of the array object, provided they exist. Moreover, if the expression P points to the last element of an array object, the expression (P)+1 points one past the last element of the array object, and if the expression Q points one past the last element of an array object, the expression (Q)-1 points to the last element of the array object. If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined. If the result points one past the last element of the array object, it shall not be used as the operand of a unary * operator that is evaluated.
То есть, на мой взгляд, стандарт говорит про UB как раз там, где описаны операции с указателями, а про subscripting говорит лишь, что это эквивалентно действиям с указателям, и все. А все UB — они от указателей.
Но: это же UB. А UB настолько undefined, что не обязано выдавать один и тот же результат даже при двух запусках. А вы тут на двух разных программах (эквивалентных, да), получаете UB, и удивляетесь, что результаты разные. Так если это UB, то результат имеет полное право быть разным, а эквивалентность программ означает только, что и там, и тут будет UB. А уж во что выльется это UB для конкретной программы, да использует ли в конкретном случае компилятор его для оптимизации, этого стандарт не обещает.
Судя по процитированному, так и надо делать. Значит UB в обоих случаях и удивляться не надо.
Constraints
1 One of the expressions shall have type ‘‘pointer to complete object type’’, the other
expression shall have integer type, and the result has type ‘‘type’’.
Semantics
2 A postfix expression followed by an expression in square brackets [] is a subscripted
designation of an element of an array object. The definition of the subscript operator []
is that E1[E2] is identical to (*((E1)+(E2))). Because of the conversion rules that
apply to the binary + operator, if E1 is an array object (equivalently, a pointer to the
initial element of an array object) and E2 is an integer, E1[E2] designates the E2-th
element of E1 (counting from zero).
Но вместе с параграфом, который вы изначально цитируете, да — и массивы и указатели — оба случая UB
Думаю, автор путает причину и следствие. Это не «железа мироздания» хватает миру впритык, это мир развивается так, чтобы использовать мирозданческую базу по максимуму.
if (table[i] == v) return i;
Скорее всего из-за того, что разработчики компилятора посчитали, что оптимизировать, используя UB возвращение булевого литерала из функции можно, а вот возвращение lvalue(не знаю, есть ли такое понятие в С) нельзя.
Другой момент: может ли компилятор доказать UB в случае с указателем? Если может, то почему не оптимизирует? Если не может, то и оптимизировать(в общем случае) возможности у него нет.
Насчет аналогии с электроном. Мне не кажется, что компилятор пытается скрыть свои оптимизации при наличии «наблюдателя». Просто обычно наличие «наблюдателя» делает невозможным оптимизацию, например, если функция не имеет побочных эффектов и её возвращаемое значения не используется, компилятор может её и не вызвать. Но если возвращаемое значение используется(«наблюдается»), то не вызывать её уже нельзя, оптимизация невозможна. Однако иногда компилятор можно поймать:
#include <iostream>
struct Electron{
char digits[10];
Electron () {
for (int i = 0; i <= 9; i++)
digits[i] = '0' + i;
}
~Electron() {
for (int i = 0; i <= 9; i++)
digits[i] = 'x';
}
};
int main () {
char array[256];
Electron* a = new(array) Electron;
for (int i = 0; i < 10; i++)
std::cout << array[i];
std::cout << "\n";
a->~Electron();
for (int i = 0; i < 10; i++)
std::cout << array[i];
std::cout << "\n";
return 0;
}
-O3:
0123456789
0123456789
-O0:
0123456789
xxxxxxxxxx
Вручную вызвать деструктор это не то же самое, что освободить память, поэтому выкидывать его просто так нельзя.
Неужели в стандарте есть про то, что на месте «уничтоженного» объекта в массиве может быть полная ерунда?
A program may end the lifetime of any object by reusing the storage which the object occupies or by explicitly calling the destructor for an object of a class type with a non-trivial destructor...При вызове деструктора завершается время жизни объекта. При завершении времени жизни объекта теряются все его данные. Компилятор для оптимизации выкидывает изменение данных, которые все равно потом теряются.
Для таких «затираний» есть специальные функции. К memset_s не может быть применена такая оптимизация, поэтому ей пользуются, когда нужно затереть пароль в памяти, после работы с ним.
Причем в случае с массивом компилятор сам понимает, что есть UB и оптимизирует(всегда возвращает true). Интересно почему он не производит аналогичную оптимизацию(например, всегда возвращать 0) в случае:
if (table[i] == v) return i;
Я не очень-то стандартовед, но в моём представлении, если за всё время выполнения в программе UB не произойдёт, то она должна отработать по стандарту. Вот если произойдёт, то тогда уже она ничего не обязана (вроде, даже до момента UB).
Всё-таки хочется верить в добрые намерения компилятора, а не "Ух ты, я нашёл UB, сейчас я им устрою". В данном случае (если я не налажал в философии в предыдущем абзаце) в примерах значение, вообще-то, могло и найтись (может, у нас инварианты такие). Но в "булевском" случае всегда, если нет UB, то возвращаемое значение true. То есть в сужении на случай "нет ни одного UB" функция честно константная. А если мы возвращаем индекс, то без UB у нас есть четыре возможных варианта.
Или я понимаю неправильно, и в приведённой функции есть UB, даже если логика программы гарантирует, что аргумент найдётся в первых четырёх элементах массива при любом вызове?
Интересно было бы узнать, так там на самом деле всё устроено или нет.
Если логика программы гарантирует, что в первых четырех элементах массива есть элемент равный аргументу при всех вызовах, то никакого UB не будет. Например:
int array[10] = {};
std::size_t size = 0;
std::cin >> size;
for (std::size_t i = 0; i < size; i++)
std::cout << array[i] << "\n";
UB такой код или нет, зависит от пользователя.
Поведение при UB сильно зависит от компилятора. Например, clang сгенерирует полноценный код с проверками за пределами массива. Если же обоим скормить -fsanitize=address
, то в рантайме оба выдадут ошибку. Причем оба компилятора будут ругаться на выражения типа table[4] = 0
, что позволяет считать отсутствие предупреждений банальным ограничением оптимизатора.
И тут обнаруживается кое-что интересное. «Идентично» — то есть, тождественно, взаимозаменяемо при любых обстоятельствах. И даже при UB?
Неопределённое поведение — оно не определено. Значит и идентичность этого поведения какому-то другому поведению при нём тоже не определена.
Это как сравнивать два поля одного nullable типа (в БД для конкретики аналогии): если они определены, то их можно сравнить (проверить на равенство). Но если хотя бы одно из них null, то это значит, что мы вообще не можем их сравнивать, результат их сравнения тоже не определён. А само null — это не состояние данных, а неопределённость этого состояния. И думать о нём нужно именно на этом абстрактном уровне. Но программисту (говорю, основываясь на личных ощущениях) очень хочется воспринимать null как очередное состояние, потому что на уровне реализации это и есть состояние, а программист чаще мыслит реализацией.
(and, in certain circumstances, an incomplete array), each of which has an optionally
specified name and possibly distinct type.»
union A {
struct BigStructWithAlignment a;
unsigned char b[sizeof( struct BigStructWithAlignment )];
};
Заполняем структурку в а, потом читаем байты из b — и все байты выравнивания как на ладошке. Никто запретить не может.
Другое дело, что нельзя надеяться на то, что байты выравнивания будут заполнены нулями.
Сейчас вот специально перечитал стандарт C++11 (3.11 Alignment) — ни слова о том, что байты выравнивания имеют какую-то особую семантику. Можете привести пример платформы, на которой байты выравнивания чем-то кардинально отличаются от байтов самой структуры?
Я помню, что где-то было про trap representation.
Но есть упоминание, что если тип данных имеет trap bits или padding bits, то сравнение таких данных побайтно (memcmp) может не сработать:
[ Note: The memcpy and memcmp semantics of the compare-and-exchange operations may result in failed comparisons for values that compare equal with operator== if the underlying type has padding bits, trap bits, or alternate representations of the same value. Thus, compare_exchange_strong should be
used with extreme care. On the other hand, compare_exchange_weak should converge rapidly. — end note ]
По сути это говорит нам, что такие биты читать можно, но нельзя надеяться, что они содержат что-то осмысленное.
Some combinations of padding bits might generate trap representations, for example, if one padding bit is a parity bit.
Если я правильно понимаю, что имеется в виду под trap, то это — аппаратное исключение процессора, которое возникает при арифметических операциях на некоторых «особых» значениях. Поискал вот тут на досуге — нашел alpha architecture reference manual, где описаны несколько видов этих аппаратных traps — overflow trap, underflow trap, invalid operation arithmetic trap, division by zero trap и еще кучка. Правда, насчет trap bits в данных ничего внятного найти не удалось.
Ох уж эти аналогии между физикой/математикой/структурой мозга и концепциями из вычислительной техники!
Компьютеры — совершенно техническая и искусственная вещь. Имхо, маловероятно, что наша вселенная оптимизирована, потому что так делают компиляторы, числа перестают складываться, потому что в JS они перестают складываться, мозг работает на 10% как компьютер, потому что иногда похоже (или кому-то показалось, что похоже), и т.п.
Эти гипотезы конечно занятны, но проверку бритвой Оккама, скорее всего, не пройдут, и вероятно, в чём-либо да содержат ошибки.
В частности, в этой статье более уместна идея ленивых вычислений, чем неопределённого поведения в С (это менее привязанный к конкретно нашей технике пример), но фраза "мы тут подумали, что вселенная вычисляется лениво" как-то банальна.
А если уж совсем резать, то нужно думать о хитром взаимодействии полей, а не вычислениях.
P.S. странно было бы делать какие-то выводы на основе аналогий, когда не доказано, что аналогии верны.
Но могу дать такой пример.
Если сознание само себя не обнаруживает, то человек счастлив.
Как только сознание начинает себя обнаруживать, счастье по троху начинает спадать.
И можно докатиться до состояние, что мозги то и заняты, что самодуманием или думанием ради думания.
Факт наблюдаемости/побочных эффектов имеет место быть.
Но сначала сделаем код реально безопасным, для чего завернем массив table в структуру так, чтобы рядом с ним гарантированно было выделенное нами место, и заход за границу массива никому не мешал, а наоборот, помогал – например, заодно проверить содержимое соседнего массива за один цикл.
When a value is stored in an object of structure or union type, including in a member
object, the bytes of the object representation that correspond to any padding bytes take
unspecified values.42) The value of a structure or union object is neveratrap
representation, even though the value of a member of the structure or union object may be
a trap representation.
У вас между двумя массивами в структуре может быть все что угодно. В том числе и 42.
То что компилятор оптимизирует A[B], но не оптимизирует *(A + B) говорит только о том что оптимизатор недостаточно умен.
Как тут уже говорили, UB заключается в самом выходе за пределы объекта. А то как именно вы выходите за пределы — уже нюансы.
Самое обидное, что это пишет человек, работающий в Intel :(
Нельзя писать вот такие штуки:
так, чтобы рядом с ним гарантированно было выделенное нами место, и заход за границу массива никому не мешал, а наоборот, помогал – например, заодно проверить содержимое соседнего массива за один цикл
Вам никто не гарантирует что за массивом начнется другой массив. Даже текущая версия компилятора не гарантирует. Поменяете настройки оптимизации и все может уползти.
Нельзя «заодно проверить содержимое соседнего массива». Соседний массив может располагаться совсем не там, где вы его ищете.
Нельзя из поведения одной конкретной версии одного компилятора делать далекоидущие выводы типа
То есть, вопреки общему заблуждению, — индекс массива и соотвествующий ему указатель для компилятора – это совершенно не одно и то же, это скорее – как электрон, который иногда проявляет свойства частицы (в границах массива), а иногда – волны (в области UB).
Это ниразу не заблуждение. Это и есть святая правда. Да, может оптимизатор хендлит эти случаи по разному. Но с точки зрения стандарта — это одно и тоже.
Но, считайте, что вы меня убедили и надо для полноты картины в начале вставить прагму компилятора, прямо запрещающую padding.
Послушайте, вы в своей статье опираетесь на стандарт, но при этом все время пытаетесь отойти от этого стандарта. Если бы вы писали не про «Язык С», а про «компилятор GCC такой-то версии, с такими то ключами, для такой то архитектуры» — я был бы согласен со всем что в статье написано.
Но вы говорите про весь язык С, а не про конкретную имплементацию. Поэтому давайте не делать необоснованных предположений.
Разве alignas(1) не будет упаковывать структуры?
since C++11
Это другой язык.
Почему вы считаете, что vikky13 говорит не о С11?
ЧТО?
В этой ветке комментариев не упоминался язык, вообще. Вы почему-то начали ссылаться на C99, мол, в нем нет стандартного способа гарантированно разместить поля без паддинга.
Я уточнил об alignas, который есть в современных версиях и С, и С++, о чём написано по моей ссылке:
As of the ISO C11 standard, the C language has the _Alignas keyword and defines alignas as a preprocessor macro expanding to the keyword in the header <stdalign.h>
If the strictest (largest) alignas on a declaration is weaker than the alignment it would have without any alignas specifiers (that is, weaker than its natural alignment or weaker than alignas on another declaration of the same object or type), the program is ill-formed
struct taddummy{
int table[0];
int table1[4];
int dummy[4];
} tadam;
Послушайте, ну это уже вообще ни в какие ворота. Да, возможно на вашей системе и с вашим компилятором там не будет пэддинга. Но на это полагаться нельзя. Вот просто нельзя писать код, который на это полагается. Совсем. В приципе. Поэтому что это UB, ага. Компилятор это знает и ведет себя соответствующим образом.
Компилятор, который между конкретно int a[4]; и int b[4]; вставит паддинг — однозначно плохой компилятор и лично я постараюсь его избегать (единственное возможное исключение, но и то не уверен — это компилятор для процессоров с троичной, 5-ричной или другой системой не степени двойки, но про них вряд ли стоит вспоминать).
Вам никто не гарантирует что за массивом начнется другой массив. Даже текущая версия компилятора не гарантирует. Поменяете настройки оптимизации и все может уползти.
Если компилятор вменяемый — не уползёт, конкретно в данном случае.
Это ниразу не заблуждение. Это и есть святая правда. Да, может оптимизатор хендлит эти случаи по разному. Но с точки зрения стандарта — это одно и тоже.
Вам говорят про компилятор, а вы отвечаете: "вы не правы, по стандарту это одно и то же, но оптимизатор (=компилятор) может и хэндлит по-разному". Не замечаете дефекта в своём ответе?
Компилятор, который между конкретно int a[4]; и int b[4]; вставит паддинг — однозначно плохой компилятор и лично я постараюсь его избегать
Согласен. Ужасный компилятор. Но в пределах стандарта.
И вообще, возможно для правильного использования кеш-линий компилятор воткнет между двумя массивами еще 12 интов. Как вам такой вариант?
Если компилятор вменяемый — не уползёт, конкретно в данном случае.
Вы в своем коде рассчитываете на поведение какого-то одного компилятора?
Вам говорят про компилятор
Статья начинается с вот этого:
Этот пост полностью соответсвует своему названию. Для начала в нем будет показано, что вопреки утверждению стандарта, а также классиков языка Си Кернигана и Ритчи, использование индексов массивов соверешенно не равнозначно использованию соответствующих указателей
Еще там есть такие вот вещи:
И объясняется это просто – по стандарту C у использования и разыменования указателей тоже есть свои случаи UB – обращение к реаллоцированной памяти, разыменование нулевого указателя и тд и тп… но никакого случая «выхода за границы массива» среди них нет и быть не может – указатели про массивы ничего не знают, вот и ведут себя вполне ожидаемым образом.
Что неправда. В стандарте указатели знают про массивы. И вот что стандарт говорит про сложение указателя с числом:
If the result points one past the last element of the array object, it
shall not be used as the operand of a unary * operator that is evaluated.
Если бы статья была только про компилятор — у меня не было бы вопросов. Но статья очень вольно интерпретирует стандарт, с чем я категорически несогласен.
Эффект был сокрушительным. С набором высоты росла скорость, движки работают как в атмосфере. И вдруг начались чудеса. Начал странным образом прыгать угол атаки, моделирование прекратилось по выходу на запредельные перегрузки.
Анализ показал, что при очень большой скорости вырос коэффициент апериодического звена управления, соответственно характерное время стало меньше шага интегрирования. Экспоненциальное по времени приближение угла атаки к заданной величине заменилось ломаной, короче модель перестала работать.
Именно тогда возникла шуточная мысль, что квантовые эффекты, обнаруженные досужими физиками имеют аналогичную природу. Создатель вселенной не предусмотрел, что ушлые высшие приматы на определенном этапе развития построят ускорители элементарных частиц и будут сильно удивлены странными нарушениями здравого смысла в закономерностях мироздания.
Наверняка такие мысли приходили многим, но я впервые это увидел в данной статье.
E1[E2] эквивалентно (*((E1)+(E2))) если E1 — массив байт.
В случае (*((E1)+(E2))) смещение равно один байт.
В общем случае:
E1[E2] эквивалентно (*((E1)+(E2)x(sizeof(ElementType)))).
Когда однажды (давным давно) мир UB сказал мне "Здравствуй и добро пожаловать", я тогда подумал, что наверно не просто так в C для возврата используют 0 в качестве OK, а всё остальное как другие состояния (например кода ошибок)…
Ну и я для себя решил, что раз при большинстве UB оно редко возвращает 0 (типа "позитивный" исход как OK), то лучше всё же придерживаться того же… Вплоть до того, что и в C++ в качестве return больше не юзал true/false (а 0/1), хотя там всё чуть сложнее. Ну и стиль определённый при написании выработался. Плюс асёрты/тест-кейсы там и всё такое.
Я вроде знаю что UB, знаю что вроде бы не определено, но карты так часто складываются… Как они с-ка "понимают", где positive а где negative case, остаётся загаткой, но так уж они себя ведут… Вели...
Потому что потом с далёкого севера пришел белый пушной зверёк вышел gcc 7.x, который с-ка перевернул "привычный" мир позитив/негатив исходов при UB и всё сломал а я потом долго искал, как с этим боротся.
Хотя бы вариант был бы чтоб оно хоть варнингом кидалось (ибо у меня всегда везде -Werror
), но это же "мы сильно много хотим"...
Так и живем теперь… как на пороховой бочке :)
static int table[2];
static int exists_in_table(int v) {
int i;
for (i = 0; i <= 2; i++) {
if (table[i] == v) return 0; /* found */
}
return 1; /* not found */
}
int main() {
return exists_in_table(42);
}
gcc 4.6.x — gcc 6.x
main:
mov eax, 1
ret
gcc 7.x — gcc trunk
main:
xor eax, eax
ret
Если предположить, что программист ничего плохого не задумал, то индекс за границы не выйдет, а значит значение всегда найдётся. Возвращаем 0.
Вот почему 1 раньше возвращалось — надо обдумать.
1. честно проверять далее.
2. оптимизировать как получится (но желательно бы с одинаковым результатом от версии к версии) и кидать варнинг (что есть очень и очень просто на константных то индексах).
более того если развернуть это, чтобы однозначно не нашел (добавить после table чего-нибудь, обнулить, убрав padding/alignment), например как здесь — gcc 7.x все-равно его «найдет».
Хотя gcc trunk уже «поумнел».
1: Достаточно часто встречается ситуация, когда правильное выполнение идет только по одному варианту, а вот веток ошибок много. Ноль сам по себе уникален в мире действительных чисел, поэтому логично единственный правильный вариант сделать нулевым.
2: В процессорах как правило есть флаг, указывающий на совпадение результата с нулем. Отсюда и унарный оператор "!", который прекрасно транслируется в минимальное количество инструкций.
а я потом долго искал, как с этим боротся
Можно попробовать использовать Undefined Behavior Sanitizer (есть в Clang и GCC). Много всего ищет (но в процессе выполнения, а не статически), хотя, наверное, не всё — например, с дефолтными настройками вызов функции через указатель с неправильной сигнатурой не определяет (это, вроде, UB):
#include <stdio.h>
int f(int x) {
return x + 1;
}
int main() {
int (*fun)(int, int) = (int (*)(int, int)) f;
int x = fun(1, 2);
printf("%d\n", x);
return 0;
}
$ clang -fsanitize=undefined test.c -o test
$ ./test
2
Подозреваю, что не весь UB можно отловить и в рантайме.
godbolt.org/g/CB8HeT
Выкинуть функцию с циклом внутри exists_in_table и возвращать всегда 1 из-за UB — пфф, легко.
Выкинуть сравнение tadam.table[5]==a. Нет, что вы, это СЛОЖНААА!
Индекс и указатель, разумеется, не идентичны, хоть и взаимозаменяемы в некоторых пределах. UB придумали марсиане. Держитесь, братья!
То есть, вопреки общему заблуждению, — индекс массива и соотвествующий ему указатель для компилятора – это совершенно не одно и то же
На самом деле — это совершенно одно и то же.
С точки зрения стандарта любая программа содержащая в исходном коде UB — не является валидной программой на языке С. Это применимо и к приведенному в статье коду.
int table[4];
bool exists_in_table(int v) {
for (int i = 0; i <= 4; i++) {
if (table[i] == v) return true;
} return false;
}
С точки зрения компилятора этот исходный код не является кодом на языке С. А значит и применять к нему утверждения «а вот в стандарте так и эдак» попросту нельзя.
В этом вся суть стандарта и если написать программу без UB, то есть программу на языке С — то там всегда индексация массива и использование указателей ведет себя одинаково.
Как пример я могу написать скрипт, который будет компилировать С/C++ или Java например в зависимости от расширения файла. Мой супер-пупер компилятор будет компилировать исходные коды аж на 3 разных языках — и я потом скажу «Компилятор компилирует исходные эти коды, то это — код на С. Иначе бы выдавалась ошибка компиляции.» Но это же полный бред. Компилятор не может классифицировать принадлежность исходного кода к какому то языку. Это делает стандарт.
И стандарт четко говорит — исходный код содержащий UB — не является кодом на языке С.
Ну мой пример, конечно, вырожденный, но и в реальной жизни есть такой пример: компиляторы С и С++. Компилятор С++ не выдает ошибку компиляции для программ написанных на языке С — но из этого не следует, что исходный код на языке С является также и исходным кодом на языке С++ только потому что компилятор не падает. Более того можно привести пример когда компилятор С++ не сможет скомпилировать программу на языке С. С точки зрения стандарта это два разных языка.
Еще раз то, что не выдается ошибка компиляции — не означает, что исходный код является кодом на языке С с точки зрения стандарта.
не является кодом на языке С.
не означает, что исходный код является кодом на языке С с точки зрения стандарта.
Это — разные вещи. Про второе обычно говорят "не соответствует стандарту" и указывают, какому именно (их же много). Первое, по хорошему, не до конца определенное утверждение, но, на мой взгляд, не следует его связывать с чем бы то ни было, кроме синтаксической корректности (она на самом деле тоже зависит от диалекта-стандарта, но тут уж ничего не поделать).
А вообще, наблюдая такие комментарии, возникает желание оформить и протолкнуть где-нить свой стандарт С, сформулированный в терминах низкоуровневых операций, производимых конструкциями языка. Где конкретно касательно указателей будет заявляться, что это просто обращение к памяти по такому-то адресу, полученному адресной арифметикой. И хоть там выходящий за границы адрес, хоть там NULL — должно быть чтение с этого адреса, а о последствиях сегфолтов будет думать уже проц и ос в рантайме. Сейчас такой режим работы вроде бы соблюдается большинством компиляторов при выключенных оптимизациях, но напрямую это никто не декларирует, а жаль.
Вы говорите, что достаточно синтаксический корректности (в первом случае), это неверно, так как программа может быть синтаксически корректна и при этом даже не компилироваться.
Автор говорит, что достаточно, чтобы программа компилировалась, что опять таки неверно, потому что компилятор вправе на UB выдавать случайный поток байт и не падать.
Все гораздо проще, то что стандарт считает кодом на языке С — то им и является. Все остальное по сути набор букв в текстовом файле, синтаксически корректно или нет уже не важно.
en.wikipedia.org/wiki/C--
Меня со вчерашнего дня мучал вопрос: а если инварианты нашей программы гарантируют, что один из первых четырёх элементов всегда совпадёт — это тоже UB? (Другой вопрос, зачем нам тогда такая функция. Не, ну может, макрос так удачно развернулся...) Например, в такой функции будет UB? — там внутри функции вообще ничего не гарантируется
int get_value_at_index(const int *array, size_t index) {
return array[index];
}
i[table] == v
Без учета UB и e1[e2] и e2[e1] эквиваленты *((e1) + (e2)). Встречал такую запись во вполне работоспособной программе. Весьма старой, правда.
А с какой стати он его вообще компилирует, если это не код на Си??
Везение ¯\_(ツ)_/¯. А если серьезно компилятор не может заранее определить есть ли UB в исходном коде, эта задача алгоритмически не разрешима.
Вы не поняли мой посыл. Я имел ввиду, что чтобы показать, что какой то компилятор противоречит стандарту (в данном случае индексация массивов и использование указателей) — надо предоставить исходный код без UB, потому что стандарт не считает программу с UB программой на языке С.
Все эти UB необходимы для огромного класса оптимизаций, но получается, что написать валидную программу на языках типа С/С++ довольно тяжело.
Так что компилятор тут не всегда может определить UB.
Массивы, указатели и другие квантовые явления вокруг нас