Беззнаковый символ a [4] [5]; а [1] [7]; неопределенное поведение?

Один из примеров неопределенного поведения из стандарта C гласит (J.2):

- Индекс массива выходит за пределы допустимого диапазона, даже если объект явно доступен с заданным индексом (как в выражении lvalue a [1] [7] при объявлении int a [4] [5]) (6.5.6)

Если объявление изменено с int a[4][5] на unsigned char a[4][5], приводит ли доступ к a[1][7] к неопределенному поведению? Я считаю, что это не так, но я слышал от других, кто не согласен, и я хотел бы узнать, что думают некоторые другие потенциальные эксперты по SO.

Мои рассуждения:

  • Согласно обычной интерпретации параграфа 4 6.2.6.1 и параграфа 7 6.5, представление объекта a - это sizeof (unsigned char [4][5])*CHAR_BIT битов, и к нему можно получить доступ как массив типа unsigned char [20], перекрытый с объектом.

  • a[1] имеет тип unsigned char [5] как lvalue, но используется в выражении (как операнд для оператора [] или, что эквивалентно, как операнд для оператора + в *(a[1]+7)), он распадается на указатель типа unsigned char *.

  • Значение a[1] также является указателем на байт «представления» a в форме unsigned char [20]. При такой интерпретации добавление 7 к a[1] действительно.


person R.. GitHub STOP HELPING ICE    schedule 22.09.2010    source источник
comment
Может быть, я упускаю что-то действительно очевидное (кто-то кричит на меня, если это так), но какая часть ваших рассуждений с использованием примера unsigned char не применима также к примеру int, описанному в стандарте?   -  person eldarerathis    schedule 22.09.2010
comment
@eldarerathis: В стандарте это не прописано так, но вам не хватает того, что люди обычно называют строгими правилами псевдонима, которые не применяются к типам символов.   -  person R.. GitHub STOP HELPING ICE    schedule 22.09.2010
comment
Более конкретно, обратите внимание, что доступ к произвольному объекту как к массиву int или некоторого другого типа, который перекрывает его, приводит к неопределенному поведению. Единственные типы, которые можно использовать таким образом, - это символьные типы (char, signed char и unsigned char).   -  person R.. GitHub STOP HELPING ICE    schedule 22.09.2010
comment
@R: Хорошо, я думаю, я понимаю, к чему вы клоните. Спасибо.   -  person eldarerathis    schedule 22.09.2010


Ответы (5)


Я бы прочитал этот «информативный пример» в J2 как намек на то, что хотел стандартный корпус: не полагайтесь на тот факт, что случайно вычисление индекса массива дает что-то внутри границ «массива представления». Цель состоит в том, чтобы гарантировать, что все отдельные границы массива всегда должны находиться в определенных диапазонах.

В частности, это позволяет реализации выполнять агрессивную проверку границ и лаять на вас либо во время компиляции, либо во время выполнения, если вы используете a[1][7].

Это рассуждение не имеет ничего общего с основным типом.

person Jens Gustedt    schedule 22.09.2010

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

Чтобы процитировать мой комментарий из нашего последнего обсуждения (Гарантирует ли C99, что массивы непрерывны? < / а>)

"Ваш исходный вопрос был для a[0][6], с объявлением char a[5][5]. Это UB, несмотря ни на что. Допустимо использовать char *p = &a[3][4]; и получить доступ p[0] к p[5]. Взятие адреса &p[6] все еще действует, но доступ к p[6] находится за пределами объекта, таким образом, UB. Доступ к a[0][6] осуществляется за пределами объекта a[0], который имеет массив типов [5] символов. Тип результата не имеет значения, важно, как вы его достигнете ».

РЕДАКТИРОВАТЬ:

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

Мне просто интересно, насколько большей ясности в рассуждениях вы ожидаете от нас, чтобы мы убедились, что это действительно UB?

РЕДАКТИРОВАТЬ 2:

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

6.3.2.1 - 3: За исключением случаев, когда это операнд оператора sizeof или унарного оператора &, или строковый литерал, используемый для инициализации массива, выражение, имеющее тип "массив типа", преобразуется в выражение с типом "указатель на тип", который указывает на начальный элемент объекта массива, а не является lvalue. Если объект массива имеет класс хранения регистров, поведение не определено.

Так что я думаю, что это действительно так:

unsigned char *p = a[1]; 
unsigned char c = p[7]; // Strict aliasing not applied for char types

Это УБ:

unsigned char c = a[1][7];

Потому что a[1] не является lvalue в этот момент, но оценивается дальше, нарушая J.2 с индексом массива вне допустимого диапазона. Что на самом деле происходит, должно зависеть от того, как компилятор на самом деле реализует индексацию массивов в многомерных массивах. Возможно, вы правы в том, что это не имеет никакого значения для каждой известной реализации. Но это тоже допустимое неопределенное поведение. ;)

person Secure    schedule 22.09.2010
comment
Если это UB, это просто своего рода теоретический UB, который никогда не может вызвать проблемы на практике, потому что использование компилятора неотличимо от использования с совершенно четко определенным поведением. Могли бы вы по-прежнему утверждать, что это был UB, если бы я сериализовал выражение указателя a[1] в строку с sprintf и прочитал его обратно в переменную указателя с sscanf, а затем добавил 7 к этому указателю? Потому что значение будет точно таким же, как если бы я преобразовал a в unsigned char * и добавил 12, и это, безусловно, четко определено. - person R.. GitHub STOP HELPING ICE; 22.09.2010
comment
Что касается вашего последнего редактирования, как насчет этих упрощений вашей действующей версии: (a[1]+0)[7] или ((unsigned char *)a[1])[7]? - person R.. GitHub STOP HELPING ICE; 23.09.2010
comment
@R ..: Да, в конце концов, это может быть, а может и нет. Это зависит от процесса оценки выражения в отношении lvalue, побочных эффектов и точек следования, и, возможно, чего-то большего. Он может даже отличаться от компилятора к компилятору. Это должно быть хорошим академическим упражнением, чтобы глубже изучить Стандарт и найти допустимые и недопустимые выражения для [1] [7]. - person Secure; 23.09.2010
comment
Факт: как только программа достигает UB, вся программа становится UB. Это не будет переопределено тем фактом, что этот конкретный UB действителен во всех известных компиляторах. Опираясь на это, позже укусит вас в какой-нибудь экзотической реализации. Мое практическое правило: если конструкция сомнительна, не используйте ее. Есть миллионы других способов делать то, что вы хотите делать, четко определенным образом. Вам никогда не нужно показывать в коде, что вы умны. Лучше покажите, что вы знакомы с хорошими принципами программной инженерии. ;) - person Secure; 23.09.2010
comment
Предположим, в стандарте указано, что программа имеет неопределенное поведение, если она удовлетворяет некоторому условию X, которое невозможно проверить. Тогда стандарт просто включает бессмысленный язык, который можно игнорировать. Если кто-то не может показать точную точку, в которой этот якобы выходящий за границы доступ к массиву становится недопустимым (то есть выражения с минимальной разницей, где одно является допустимым, а другое - нет), я считаю, что это бессмысленный язык в стандарте - что не только маловероятно, но и невозможно написать компилятор, который удовлетворяет другим условиям стандарта, но отклоняет a[1][7]. - person R.. GitHub STOP HELPING ICE; 23.09.2010
comment
@R ..: Неважно. Вы не пишете код, использующий неопределенное поведение. Даже если он работает на всех платформах всегда, в лучшем случае для следующего человека все равно будет сложно взглянуть на ваш источник. По крайней мере, я был бы чертовски сбит с толку, если бы объявили [3] [4] и получили доступ к [1] ​​[7]. Когда я запутываюсь, я врываюсь к вам в офис, жду, пока вы не объясните, почему вы это сделали, затем исправляю код и фиксирую с помощью R .., запутанного в комментарии коммита. (Шучу, но только частично.) - person DevSolar; 24.09.2010
comment
использование компилятора неотличимо от использования с совершенно четко определенным поведением. (...) Тогда стандарт просто включает бессмысленный язык, который вы можете игнорировать. Общий Аргумент верен, но в данном случае вы ошибаетесь: что, если компилятор использует это стандартное предложение для облегчения анализа псевдонимов? Думайте о типах массивов как о ограничении. - person curiousguy; 04.10.2011
comment
@curiousguy: Большинство авторов C89 никогда бы даже не предположили, что компиляторы будут способны к тем видам оптимизации, которые они делают сегодня, и не видели необходимости описывать точные пределы того, какие оптимизации компилятор может и не может делать. Ключевой особенностью дизайна C с самого начала было то, что, имея указатель на первый элемент int[5][5];, можно было легко написать код для доступа ко всему массиву. Стандарт определил несколько способов написания такого кода как эквивалентные, а затем заявил, что один из них вызывает UB. Если бы авторы Стандарта не хотели ослабевать ... - person supercat; 08.01.2017
comment
... язык, они должны были указать, что некоторые из способов написания кода могут рассматриваться как эквивалентные другим для удобства компилятора, но могут позволить компилятору делать оптимизационные предположения, которые не быть разрешенным для других. Наилучшим ИМХО было бы сказать, что с учетом int(*a)[5] выражения a[n] и &(a[n]) позволили бы компилятору предположить, что n было 0..4, но компилятор не мог предположить то же самое для *(a+n) или (a+n). К сожалению, я не думаю, что что-либо в Стандарте говорит об этом. - person supercat; 08.01.2017

С 6.5.6 / 8

Если и операнд-указатель, и результат указывают на элементы одного и того же объекта массива или один за последним элементом объекта массива, оценка не должна приводить к переполнению; в противном случае поведение не определено.

В вашем примере a [1] [7] не указывает ни на тот же объект массива a [1], ни на один за последним элементом a [1], поэтому это поведение undefined.

person czchen    schedule 22.09.2010
comment
Я утверждаю, что есть еще один массив, который я назову (за отсутствием лучшего слова) массивом представления объекта в форме unsigned char [sizeof object]. Поскольку a[1] распадается на указатель, который также является указателем на этот массив представления, и a[1]+7 также находится в пределах этого массива, я утверждаю, что a[1][7] четко определен. - person R.. GitHub STOP HELPING ICE; 22.09.2010
comment
@R ..: Конечно, конструкция a [1] [7] имеет смысл на уровне машинного кода, ячеек памяти. Это не меняет того факта, что 7 - это индекс массива вне допустимого диапазона на уровне языка C. - person DevSolar; 24.09.2010
comment
@R: Хотя [1] распадается на указатель, который представляет тот же адрес, что и ((unsigned char *) a) +5, указатель не того же типа, и ничто не запрещает компилятору сохранять каждый указатель в качестве базы и limit, и проверка лимита на все операции индексирования и разыменования. - person supercat; 16.10.2010
comment
Этот массив представлений - это просто то, что вы придумали. Это не требуется стандартом, поэтому любые рассуждения, требующие этого, также не требуются стандартом. Ваш компилятор может просто так работать, а мой - нет. То, что зависит от того, как работает ваш компилятор, - это определение неопределенного (или неопределенного) поведения. - person David Schwartz; 15.08.2011

Под капотом, на реальном машинном языке, нет никакой разницы между a[1][7] и a[2][2] для определения int a[4][5]. Как сказал R .., это связано с тем, что доступ к массиву преобразуется в 1 * sizeof(a[0]) + 7 = 12 и 2 * sizeof(a[0]) + 2 = 12 (конечно, * sizeof(int)). Машинный язык ничего не знает о массивах, матрицах или индексах. Все он знает об адресах. Вышеупомянутый компилятор C может делать все, что ему заблагорассудится, в том числе наивную базу проверки границ в индексаторе - тогда a[1][7] будет вне границ, потому что массив a[1] не имеет 8 ячеек. В этом отношении нет разницы между int и char или unsigned char.

Я предполагаю, что разница заключается в строгих правилах псевдонима между int и char - хотя программист на самом деле не делает ничего плохого, компилятор вынужден выполнить "логическое" приведение типа для массива, чего он не должен делать. . Как сказал Йенс Густедт, это больше похоже на способ включить строгие проверки границ, а не на реальную проблему с int или char.

Я немного поигрался с компилятором VC ++, и, похоже, он ведет себя так, как вы ожидаете. Кто-нибудь может проверить это с gcc? По моему опыту, gcc гораздо строже в подобных вещах.

person Eli Iser    schedule 22.09.2010

Я считаю, что причина, по которой процитированный образец (J.2) имеет неопределенное поведение, заключается в том, что компоновщику не требуется помещать подмассивы a [1], a [2] и т. Д. Рядом друг с другом в памяти. Они могут быть разбросаны по памяти или могут быть смежными, но не в ожидаемом порядке. Переключение базового типа с int на unsigned char ничего из этого не меняет.

person AlcubierreDrive    schedule 22.09.2010
comment
Нет, они обязательно должны быть смежными по памяти. Подумайте, как работают sizeof и memcpy. Насколько я могу судить, причина в алиасинге. Вы хотите, чтобы компилятор мог предположить, что a[0][i] и a[1][j] никогда не указывают на одно и то же место. Но все ставки сделаны на типы персонажей. Указатель на символьный тип всегда может быть псевдонимом других указателей (на любой тип), если только ключевое слово restrict не используется, чтобы сообщить компилятору, что этого не будет. - person R.. GitHub STOP HELPING ICE; 22.09.2010
comment
Компоновщик никогда не определяет макет объекта. - person curiousguy; 04.10.2011