Как стать автором
Обновить

Комментарии 105

И тут обнаруживается кое-что интересное. «Идентично» — то есть, тождественно, взаимозаменяемо при любых обстоятельствах. И даже при UB?

Значит в стандарте противоречие?
По идее, тогда компилятор должен выбирать специфицированный (одинаковое поведение), вместо неспецифицированного (UB) варианта.
Это как если бы область видимости методов ограничивалась при использовании private. Если его изменить на public, то работа корректного кода может измениться.
По-моему, компиляторы слишком увлеклись оптимизацией при обнаружении UB — в случае массива гораздо полезнее было бы предупреждение.
компиляторы слишком увлеклись оптимизацией при обнаружении UB — в случае массива гораздо полезнее было бы предупреждение

Я тоже так сначала подумал.

Но это ж C. Не хочется UB, нужно использовать что-то более высокоуровневое.
Ну это не всегда возможно.
А вот компилятор, раз уж такой умный, мог бы сделать Си чуть получше.
Не компилятор тогда, а стандарт. Компилятор следует стандарту, если написано что разрешено — значит тебе дают выбор. А с точки зрения разработчиков компилятора, да и других людей, «лучшее» поведение может трактоваться по разному.
Неужели UB настолько часто используется в реальных программах и не имеет полезного побочного эффекта, что лучшим выбором будет удалить этот код?
Тут уже неоднократно были споры о нужности UB. Как ни странно именно наличие UB позволяет компилировать оптимальный код для самых разных платформ.
Тут вопрос не в том, зачем в стандарте UB. Это как раз понятно.
Вопрос в том, почему, если компилятор видит, что код явно с ошибкой, то он его выкидывает, а не сообщение выводит?
Потому что согласно стандарта это не ошибка. Если компилятор прервет сборку в этом случае, то это будет нарушением стандарта.

В gcc можно включить такие предупреждения с помощью -Warray-bounds и заставить его останавливать сборку при наличии любых предупреждений с помощью -Werrors

Простите, я не разбираюсь в си.


У меня есть предположение, что концепция ub была введена, чтобы компилятор работал быстрее и не тратил ресурсы на поиск неадекватного кода. Насколько адекватно моё предположение?


И связанный вопрос: замедляется ли (или замедлялась ли в прошлом) сборка проекта при этих флагах?

UB нужен, чтобы не описывать правила, которые нельзя (или дорого) проверить.
А раньше компиляторы и не умели даже в простых случаях выход за границы массива детектить, это только недавно появилось.
UB нужен, чтобы оптимизатору было проще оптимизировать код. То есть, чтобы при оптимизациях не нужно было поддерживать работоспособность кода, работающего через UB, а значит можно сделать больше оптимизаций, которые корректны для не-UB кода. Время работы самого компилятора тут ни при чём, и наверно никогда не было при чём. В нормальных компиляторах можно многие UB сделать не-UB, не включая соответствующие оптимизации. Например, целое переполнение — тоже UB, если вы используете gcc с -fstrict-overflow (он включается с -O2), который оптимизирует арифметику, и не UB (начинает работать по обычным законам двоичной арифметики с ограниченой разрядностью), если -fno-strict-overflow.
Возьмём пример из статьи и поменяем 4 на 3. Во многих процессорах есть SIMD инструкции как раз на 4 — 8 чисел. И память выделяется с выравниванием по степени двойки. Мы не можем явно вызывать SIMD интринсики в коде, который будет работать на самом разном железе, но можем дать подсказку компилятору.
Но не то поведение, когда функция поиска числа в массиве выдает неверный ответ. Я конечно, смотря с вашего уровня, профан в программировании, но продемонстрированный пример с массивом — это даже не электрон, а черт знает что. А потом всякие спектры и мелтдауны заводятся от таких неопределенных бехевиоров!
Ага, в правильной программе не должно быть UB, так что симуляция еще и забагованная.
Тоже где-то на хабре встречал комментарий примерно следующего содержания:
Мы живем в симуляции. Максимальное значение переменной — скорость света, младшие биты — квантовые эффекты.
Сразу оговорюсь, доступа к официальному тексту стандарта у меня нет, я смотрю по «N1570, Committee Draft — April 12, 2011 ISO/IEC 9899:201x». Вроде бы, это последний драфт, близкий к финальному тексту.

А теперь давайте хоть однажды в жизни еще раз почитаем стандарт С.
Да, в разделе, посвященном массивам, он честно предупреждает, что доступ к элементам за границами массива – это 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 в обоих случаях и удивляться не надо.
У меня — еще более ранняя редакция. Посмотрела вашу редакцию стандарта — там тоже это есть 6.5.2.1 Array subscripting
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
Я скорее имел в виду, что массивы дают тут UB именно потому, что E1[E2] эквивалентно *((E1)+(E2)), а UB описано для сложения указателя и целого. А отдельного UB для обращения к элементам массива не описано, это то же самое UB (в смысле, описанное в том же месте).
Тут есть еще интересный момент. Компилятору в случае E1[E2] проверить выход за границу массива и, при желании, оптимизировать, элементарно. А вот как понять, что за границу массива вышел указатель? Компилятор единственное, что может сделать — разыменовать его и или дать элемент оттуда или, если это недоступная область памяти, выдать исключение (ошибку). Тем более, если я не как в примере возьму само имя массива в качестве указателя, а просто возьму «левый» указатель и присвою ему адрес начала массива. То есть, на практике UB в этом случае не будет никогда — только в теории. А для массивов — как видите, есть и в реальности
Ну вот ровно поэтому поведение и отличается для массива и указателя. Только то, что «на практике UB в этом случае не будет никогда» — это неверно. UB тут в обоих случаях. Просто в простом случае (массивы) компиляторы уже научились это распознавать и выдавать не совсем ожидаемое поведение, а в сложном (указатель) — еще нет, и поведение более соответствует интуиции. Но это в любом случае UB, и нет гарантии, что сложный случай в будущем тоже не даст какие-то фокусы.
> оптимизация нужна там, где ресурсов железа хватает «впритык» без запаса

Думаю, автор путает причину и следствие.   Это не «железа мироздания» хватает миру впритык, это мир развивается так, чтобы использовать мирозданческую базу по максимуму.
И в случае с указателем, и в случае с массивом происходит UB. Причем в случае с массивом компилятор сам понимает, что есть UB и оптимизирует(всегда возвращает true). Интересно почему он не производит аналогичную оптимизацию(например, всегда возвращать 0) в случае:
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

Это уже явный беспредел.
Вручную вызвать деструктор это не то же самое, что освободить память, поэтому выкидывать его просто так нельзя.
Неужели в стандарте есть про то, что на месте «уничтоженного» объекта в массиве может быть полная ерунда?
Да, есть. n4567 (3.8.4)
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, или всегда true» и заменить на «всегда true». А в небулевом случае ничего доказать нельзя.

Если логика программы гарантирует, что в первых четырех элементах массива есть элемент равный аргументу при всех вызовах, то никакого 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 как очередное состояние, потому что на уровне реализации это и есть состояние, а программист чаще мыслит реализацией.

Hеопределённость состояния — это undefined, а null — это вполне себе определённое значение, просто оно null :)

НЛО прилетело и опубликовало эту надпись здесь
Это — не особенность реализации, это — ваша особенность :) В смысле — у вас какие-то особенные структуры или особенный С. Цитата из стандарта «A structure type describes a sequentially allocated nonempty set of member objects
(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 — и все байты выравнивания как на ладошке. Никто запретить не может.
Другое дело, что нельзя надеяться на то, что байты выравнивания будут заполнены нулями.
А вот какая там будет память зависит от платформы. Стандарт ничего не гарантирует.
Мне кажется, вы перегибаете палку. Когда под структуру выделяется память (через malloc, new или объявление переменной), то стандарт гарантирует выделение куска памяти размером не менее sizeof(структура). И правила доступа ко всему этому куску будут одинаковыми — другими словами, если вы можете прочитать два значения из структуры, то вы сможете прочитать и байты выравнивания, расположенные между ними.
Сейчас вот специально перечитал стандарт C++11 (3.11 Alignment) — ни слова о том, что байты выравнивания имеют какую-то особую семантику. Можете привести пример платформы, на которой байты выравнивания чем-то кардинально отличаются от байтов самой структуры?
Может побайтовое чтение и законно.
Я помню, что где-то было про trap representation.
Поискал в стандарте по слову trap — нашел всего 8 упоминаний. В основном — про то, что некоторые типы данных могут вызывать trap при арифметических операциях на некоторых значениях (нутром чую, что имеется в виду что-то типа аппаратного исключения при делении на ноль). Про механизм работы этих traps нет ни слова.
Но есть упоминание, что если тип данных имеет 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 ]


По сути это говорит нам, что такие биты читать можно, но нельзя надеяться, что они содержат что-то осмысленное.
У меня есть какой-то Draft C99. Там про целые числа такое есть, когда у переменной есть не только значащие биты (не знаю что это за архитектуры):

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 в данных ничего внятного найти не удалось.
Так я же выше согласился с корректностью побайтового чтения — в стандарте тоже про это что-то сказано.
Не уверен, что это напрямую относится к slop'у, но вот неинициализированные переменные с плавающей точкой на стеке да, могут вызывать аппаратные исключения, если в них случайно окажется недопустимое значение.

Ох уж эти аналогии между физикой/математикой/структурой мозга и концепциями из вычислительной техники!
Компьютеры — совершенно техническая и искусственная вещь. Имхо, маловероятно, что наша вселенная оптимизирована, потому что так делают компиляторы, числа перестают складываться, потому что в 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 :(

Нужно обладать достаточно специфичным складом ума чтобы правильно читать и понимать стандарты :)
В моей структуре между двумя массивами без специальных ухищрений (=ключей компилятора) — не может. Это легко проверяемо. А в общем случае — да, бывает padding. Но мы его не рассматриваем.
Послушайте, ну это уже вообще ни в какие ворота. Да, возможно на вашей системе и с вашим компилятором там не будет пэддинга. Но на это полагаться нельзя. Вот просто нельзя писать код, который на это полагается. Совсем. В приципе. Поэтому что это UB, ага. Компилятор это знает и ведет себя соответствующим образом.

Нельзя писать вот такие штуки:

так, чтобы рядом с ним гарантированно было выделенное нами место, и заход за границу массива никому не мешал, а наоборот, помогал – например, заодно проверить содержимое соседнего массива за один цикл


Вам никто не гарантирует что за массивом начнется другой массив. Даже текущая версия компилятора не гарантирует. Поменяете настройки оптимизации и все может уползти.

Нельзя «заодно проверить содержимое соседнего массива». Соседний массив может располагаться совсем не там, где вы его ищете.

Нельзя из поведения одной конкретной версии одного компилятора делать далекоидущие выводы типа

То есть, вопреки общему заблуждению, — индекс массива и соотвествующий ему указатель для компилятора – это совершенно не одно и то же, это скорее – как электрон, который иногда проявляет свойства частицы (в границах массива), а иногда – волны (в области UB).


Это ниразу не заблуждение. Это и есть святая правда. Да, может оптимизатор хендлит эти случаи по разному. Но с точки зрения стандарта — это одно и тоже.
Говоря про мой случай, я имею в виду не конкретный компилятор и систему, а то, что у меня подряд 2 массива из 4 int каждый. В таких условиях я не могу представить ни одной ситуации, когда компилятору зачем-либо понадобится вставить между ними что-либо. Перед первым — да, между — нет. Если вы сможете ее придумать, буду благодарна.
Но, считайте, что вы меня убедили и надо для полноты картины в начале вставить прагму компилятора, прямо запрещающую padding.
Стандарт С99 определяет ровно три прагмы, ни одна из которых не влияет на упаковку структур. Все остальные прагмы implementation-defined и каждый компилятор может реагировать на их по своему.

Послушайте, вы в своей статье опираетесь на стандарт, но при этом все время пытаетесь отойти от этого стандарта. Если бы вы писали не про «Язык С», а про «компилятор GCC такой-то версии, с такими то ключами, для такой то архитектуры» — я был бы согласен со всем что в статье написано.
Но вы говорите про весь язык С, а не про конкретную имплементацию. Поэтому давайте не делать необоснованных предположений.
image

Разве 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>
Да, действительно. С С11 я провтыкал. Но в любом случае alignas может только увеличить пэддинг:

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;
Самое интересное из всех наблюдений. Спасибо! видимо, логика компилятора тут будет «раз вы сами так наплевательски к оптимизации относитесь, то и я вам ничего оптимизировать не буду» :)
Нулевые массивы созданы для структур переменной длины (не во всех компиляторах они есть, но в современных присутствуют).
{

int data[0];
};
Послушайте, ну это уже вообще ни в какие ворота. Да, возможно на вашей системе и с вашим компилятором там не будет пэддинга. Но на это полагаться нельзя. Вот просто нельзя писать код, который на это полагается. Совсем. В приципе. Поэтому что это 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.


Если бы статья была только про компилятор — у меня не было бы вопросов. Но статья очень вольно интерпретирует стандарт, с чем я категорически несогласен.
Дело было в 80-х годах, написал самодельный авиасимулятор (убогий по нынешним временам, техника была тогда слабая). Ну и развлекались с коллегами. В порядке эксперимента задал ВСХ (высотно-скоростные характеристики) двигателя, не зависящие от высоты, т.е. плотности атмосферы. Т.е. двигатель как-бы может работать без атмосферного воздуха. А программа атмосферных параметров использовалась реальная. Вот думаю, полетаю на супер-пупер фантастическом аппарате в ближнем космосе.

Эффект был сокрушительным. С набором высоты росла скорость, движки работают как в атмосфере. И вдруг начались чудеса. Начал странным образом прыгать угол атаки, моделирование прекратилось по выходу на запредельные перегрузки.

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

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

Наверняка такие мысли приходили многим, но я впервые это увидел в данной статье.

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 раньше возвращалось — надо обдумать.
Если индекс не выйдет за границы, то из этого совсем не следует, что значение найдется.
А какой ещё сценарий выполнения, когда индекс не выходит?
честно проверять все элементы массива и дальше — или встречу динозавра или нет :)
Оптимизировать до границы массива (т.е. выкинуть из цикла проверку на индексах 0..1), а на UB (при нахождении out-of-range)…
  1. честно проверять далее.
  2. оптимизировать как получится (но желательно бы с одинаковым результатом от версии к версии) и кидать варнинг (что есть очень и очень просто на константных то индексах).
Совершенно верно...,
более того если развернуть это, чтобы однозначно не нашел (добавить после table чего-нибудь, обнулить, убрав padding/alignment), например как здесь — gcc 7.x все-равно его «найдет».
Хотя gcc trunk уже «поумнел».
Мне представляются два аргумента за использование 0 в качестве «ОК».

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 выдавать случайный поток байт и не падать.

Все гораздо проще, то что стандарт считает кодом на языке С — то им и является. Все остальное по сути набор букв в текстовом файле, синтаксически корректно или нет уже не важно.
На самом деле, спорить тут бесполезно — это вопрос из серии «Является ли фраза „варкалось, хливкие шорьки пырялись по мове“ написанной на русском языке или нет?» Но даже в вашей трактовке — код с UB -именно часть стандарта, так как он там есть. Т.е. в стандарте С нет ничего про нарисованные смайлики или ключевое слово class тк это — не часть языка С. А вот про выход за границу массива есть — именно что в этом случае поведение неопределено, т.е. это — часть языка.
Про C-- подробностей не знаю, но может он как раз такой?
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)). Встречал такую запись во вполне работоспособной программе. Весьма старой, правда.
i[«ABCD»] вполне встречалось.
А с какой стати он его вообще компилирует, если это не код на Си??


Везение ¯\_(ツ)_/¯. А если серьезно компилятор не может заранее определить есть ли UB в исходном коде, эта задача алгоритмически не разрешима.

Вы не поняли мой посыл. Я имел ввиду, что чтобы показать, что какой то компилятор противоречит стандарту (в данном случае индексация массивов и использование указателей) — надо предоставить исходный код без UB, потому что стандарт не считает программу с UB программой на языке С.
Вообще, некоторые стандарты по UB выглядят странно. С той же Вики: функция без return, инкремент и сложение — бросить ошибку при компиляции, и все дела. То же и с массивом константного размера с итерацией в цикле, заданном константами. Выглядит как троллинг.
У компиляторов есть техники обнаружения UB (например функция без return 100% детектится как warning), и в случае с -Werror они выдают ошибку компиляции. Другое дело когда UB слишком хитрое и не очевидное для компилятора. В таком случае может помочь static analysis tools и то не всегда.

Все эти UB необходимы для огромного класса оптимизаций, но получается, что написать валидную программу на языках типа С/С++ довольно тяжело.
bvdmitri, Dovgaluk, да уж… Видимо, стат-анализаторы — наше всё.
Инкремент и присваивание могут обращаться к одной ячейке через разные указатели.
Так что компилятор тут не всегда может определить UB.
Кстати есть интересный проект UndefinedBehaviourSanitizer. По заявлениям может помочь в поиске UB в рантайме, но сильно бьет по производительности
Чепуха какая-то! Ну выдала программа неправильный результат в ответ на некорректный код. И что? С такими талантами лучше писать статьи для «желтой прессы». Статья бесполезная, не понимаю откуда такой рейтинг, уж не накрутка ли?
Нормальный рейтинг для хорошего юмористического рассказа.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий