Взять адрес элемента массива «один за другим» через индекс: допустимый по стандарту C ++ или нет? - PullRequest
72 голосов
/ 12 июня 2009

Я уже несколько раз утверждал, что следующий код не разрешен стандартом C ++:

int array[5];
int *array_begin = &array[0];
int *array_end = &array[5];

Является ли &array[5] допустимым кодом C ++ в этом контексте?

Я бы хотел получить ответ со ссылкой на стандарт, если это возможно.

Было бы также интересно узнать, соответствует ли он стандарту С. И если это не стандартный C ++, почему было принято решение рассматривать его иначе, чем array + 5 или &array[4] + 1?

Ответы [ 13 ]

39 голосов
/ 12 июня 2009

Да, это законно. Из проекта стандарта C99 :

§6.5.2.1, пункт 2:

Выражение с постфиксом, за которым следует выражение в квадратных скобках [] является индексом обозначение элемента объекта массива. Определение индекса оператора [] является то, что E1[E2] идентичен (*((E1)+(E2))). Из-за правил преобразования, которые применяется к бинарному оператору +, если E1 является объектом массива (эквивалентно указателю на начальный элемент объекта массива) и E2 является целым числом, E1[E2] обозначает E2 элемент E1 (считая с нуля).

§6.5.3.2, пункт 3 (выделено мной):

Унарный оператор & возвращает адрес своего операнда. Если операнд имеет тип ‘‘ type ’’, результат имеет тип ‘‘ указатель на тип ’’. Если операнд является результатом унарного * оператора, ни этот оператор, ни оператор & не оцениваются, и результат такой, как если бы оба опущено, за исключением того, что ограничения на операторы все еще применяются, и результат не именующий. Точно так же, , если операнд является результатом оператора [], ни оператор &, ни унарный *, подразумеваемый [], не оцениваются, и результат такой, как если бы оператор & были удалены, а оператор [] был заменен на оператор + . В противном случае результат указатель на объект или функцию, обозначенный его операндом.

§6.5.6, пункт 8:

Когда выражение, имеющее целочисленный тип, добавляется или вычитается из указателя, результат имеет тип операнда указателя. Если операнд указателя указывает на элемент объект массива, и массив достаточно велик, результат указывает на смещение элемента от исходный элемент такой, что разница индексов полученного и исходного элементы массива равны целочисленному выражению. Другими словами, если выражение P указывает на i -й элемент массива, выражения (P)+N (эквивалентно N+(P)) и (P)-N (где N имеет значение n) указывают соответственно на i+n -й и i−n -й элементы объект массива, если они существуют. Более того, если выражение P указывает на последний элемент массива, выражение (P)+1 указывает на один последний элемент объект массива, и если выражение Q указывает один за последним элементом объекта массива, выражение (Q)-1 указывает на последний элемент объекта массива. Если оба указателя операнд и результат указывают на элементы одного и того же объекта массива или один за последним элемент массива, при оценке не должно быть переполнения; в противном случае поведение не определено. Если результат указывает на один последний элемент массива, он не должен использоваться как операнд унарного * оцениваемого оператора.

Обратите внимание, что стандарт явно позволяет указателям указывать один элемент за концом массива, при условии, что они не разыменовываются . В 6.5.2.1 и 6.5.3.2 выражение &array[5] эквивалентно &*(array + 5), что эквивалентно (array+5), что указывает на один конец конца массива. Это не приводит к разыменованию (на 6.5.3.2), поэтому это законно.

36 голосов
/ 12 июня 2009

Ваш пример допустим, но только потому, что вы на самом деле не используете указатель вне границ.

Давайте сначала разберемся с указателями за пределами границ (потому что именно так я первоначально интерпретировал ваш вопрос, прежде чем заметил, что в примере вместо этого используется указатель «один за другим»):

В общем, вам даже не разрешено создавать указатель вне пределов. Указатель должен указывать на элемент в массиве или один за концом . Нигде больше.

Указатель даже не может существовать, что означает, что вам явно не разрешается разыменовывать его.

Вот что говорит стандарт по этому вопросу:

5,7: 5:

Когда выражение, которое имеет интеграл тип добавляется или вычитается из указатель, результат имеет тип операнд указателя. Если указатель операнд указывает на элемент массив объекта, а массив большой достаточно, результат указывает на смещение элемента от оригинала элемент такой, что разница индексы полученного и оригинальные элементы массива равны интегральное выражение. Другими словами, если выражение P указывает на i-й элемент объекта массива, выражения (P) + N (эквивалентно, N + (P)) и (P) -N (где N имеет значение n) указывают соответственно на i + n-й и i-n-й элементы объект массива, если они существуют. Более того, если выражение P указывает до последнего элемента массива объект, выражение (P) +1 баллов один за последним элементом массива объект, и если выражение Q указывает один за последним элементом массива объект, выражение (Q) -1 указывает на последний элемент объекта массива. Если и операнд указателя, и результат указывают на элементы одного и того же объект массива или один за последним элемент массива объекта, оценка не должна давать над потоком; в противном случае поведение недеформированной определено .

(акцент мой)

Конечно, это для оператора +. Поэтому, чтобы быть уверенным, вот что стандарт говорит о подписке на массив:

5.2.1: 1

Выражение E1[E2] идентично (по определению) *((E1)+(E2))

Конечно, есть очевидное предостережение: ваш пример на самом деле не показывает вне границ указатель. он использует указатель «один за концом», который отличается. Указатель может существовать (как сказано выше), но стандарт, насколько я могу судить, ничего не говорит о его разыменовании. Самое близкое, что я могу найти, это 3.9.2: 3:

[Примечание: например, адрес за концом массива (5.7) будет считаться указывают на несвязанный объект типа элемента массива, который может быть расположен по этому адресу. —Конечная записка]

Мне кажется, что да, вы можете юридически разыменовать его, но результат чтения или записи в это местоположение не указан.

Спасибо ilproxyil за исправление последнего бита здесь, отвечая на последнюю часть вашего вопроса:

  • array + 5 на самом деле не разыскивать что угодно, это просто создает указатель на один конец array.
  • &array[4] + 1 разыменования array+4 (что совершенно безопасно), берет адрес этого lvalue, и добавляет один к этому адресу, который приводит к указателю «один за другим» (но этот указатель никогда не получает разыменованный.
  • &array[5] массив разыменований + 5 (что, насколько я вижу, является законным, и приводит к «не связанному объекту типа элемента массива ", как выше сказанного), а затем принимает адрес этого элемента, который также кажется достаточно законным.

Так что они не делают одно и то же, хотя в этом случае конечный результат тот же.

17 голосов
/ 12 июня 2009

Это является законным.

Согласно документации gcc для C ++ , &array[5] является законным. И в C ++ , и в C вы можете безопасно обращаться к элементу, который находится за концом массива, - вы получите правильный указатель. Так что &array[5] как выражение допустимо.

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

На практике, я полагаю, что обычно это не приводит к сбою.

Edit: Кстати, это, как правило, как итератор end () для контейнеров STL (как указатель на один конец), так что это довольно хорошее свидетельство того, что практика является законной. 1017 *

Редактировать: О, теперь я вижу, что вы на самом деле не спрашиваете, допустимо ли удерживать указатель на этот адрес, но если этот точный способ получения указателя является законным. Я передам это другим ответчикам.

9 голосов
/ 12 июня 2009

Я считаю, что это законно, и это зависит от происходящего преобразования 'lvalue to rvalue'. Последняя строка Основная проблема 232 имеет следующее:

Мы согласились, что подход в стандарте кажется нормальным: p = 0; *п; по сути не является ошибкой. Преобразование lvalue в rvalue даст ему неопределенное поведение

Хотя это немного другой пример, он показывает, что '*' не приводит к преобразованию lvalue в rvalue и, таким образом, учитывая, что выражение является непосредственным операндом '&', который ожидает lvalue, а затем поведение определяется.

8 голосов
/ 12 июня 2009

Я не верю, что это незаконно, но я верю, что поведение & array [5] не определено.

  • 5.2.1 [expr.sub] E1 [E2] идентичен (по определению) * ((E1) + (E2))

  • 5.3.1 [expr.unary.op] унарный оператор * ... результатом является lvalue, указывающее на объект или функцию, на которые указывает выражение.

На данный момент у вас неопределенное поведение, потому что выражение ((E1) + (E2)) на самом деле не указывает на объект, и стандарт действительно говорит, каким должен быть результат, если он этого не делает.

  • 1.3.12 [defns.undefined] Неопределенное поведение также может ожидаться, когда в этом международном стандарте опущено описание любого явного определения поведения.

Как отмечено в другом месте, array + 5 и &array[0] + 5 являются допустимыми и четко определенными способами получения указателя один за концом массива.

6 голосов
/ 12 июня 2009

В дополнение к вышеприведенным ответам я укажу оператор & может быть переопределен для классов. Поэтому, даже если он действителен для POD, это, вероятно, не очень хорошая идея для объекта, который, как вы знаете, недопустим (во многом как переопределение оператора & () в первую очередь).

4 голосов
/ 13 июня 2009

Это законно:

int array[5];
int *array_begin = &array[0];
int *array_end = &array[5];

Раздел 5.2.1 Подписка Выражение E1 [E2] идентично (по определению) * ((E1) + (E2))

Таким образом, мы можем сказать, что array_end также эквивалентен:

int *array_end = &(*((array) + 5)); // or &(*(array + 5))

Раздел 5.3.1.1 Унарный оператор '*': унарный оператор * выполняет косвенное обращение: выражение, к которому он применяется, должно быть указателем на тип объекта или указатель на тип функции и результатом является lvalue, ссылающееся на объект или функцию , на которые указывает выражение. Если тип выражения «указатель на T», тип результата - «T». [Примечание: указатель на неполный тип (другое чем cv void) может быть разыменовано. Полученное таким образом значение может быть использовано ограниченным образом (для инициализации ссылки, для пример); это значение не должно быть преобразовано в значение, см. 4.1. - конец примечания]

Важная часть вышесказанного:

«результатом является lvalue, ссылающееся на объект или функцию».

Унарный оператор '*' возвращает lvalue со ссылкой на int (без разыменования). Затем унарный оператор '&' получает адрес lvalue.

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

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

В разделе комментариев ниже представлены два аргумента:

(пожалуйста, прочитайте: но это долго, и мы оба в конечном итоге троллить)

Аргумент 1

это незаконно из-за параграфа 5.7 параграфа 5

Когда выражение с целочисленным типом добавляется или вычитается из указателя, результат имеет тип операнда указателя. Если операнд-указатель указывает на элемент объекта массива, а массив достаточно велик, результат указывает на смещение элемента от исходного элемента, так что разность индексов результирующего и исходного элементов массива равна интегральному выражению. Другими словами, если выражение P указывает на i-й элемент объекта массива, выражения (P) + N (эквивалентно, N + (P)) и (P) -N (где N имеет значение n) указывают соответственно i + n-м и i-n-м элементам массива, если они существуют. Кроме того, если выражение P указывает на последний элемент объекта массива, выражение (P) +1 указывает один за последним элементом объекта массива, а если выражение Q указывает на один последний элемент последнего элемента массива, выражение (Q) -1 указывает на последний элемент объекта массива. Если и операнд-указатель, и результат указывают на элементы одного и того же объекта массива или одного последнего последний элемент объекта массива, оценка не должна производить переполнение; в противном случае поведение не определено.

И хотя раздел актуален; это не показывает неопределенное поведение. Все элементы в массиве, о котором мы говорим, находятся либо внутри массива, либо за концом (что хорошо определено в предыдущем абзаце).

Аргумент 2:

Второй аргумент, представленный ниже: * - оператор разыменования.
И хотя это общий термин, используемый для описания оператора *; этот термин намеренно избегают в стандарте, так как термин «отсылка» недостаточно четко определен с точки зрения языка и того, что это означает для базового оборудования.

ХотьОбращение с памятью за пределами массива определенно является неопределенным поведением. Я не уверен, что unary * operator обращается к памяти (читает / записывает в память) в этом контексте (не так, как определяет стандарт). В этом контексте (как определено стандартом (см. 5.3.1.1)) unary * operator возвращает lvalue referring to the object. В моем понимании языка это не доступ к основной памяти. Результат этого выражения затем немедленно используется оператором unary & operator, который возвращает адрес объекта, на который ссылается lvalue referring to the object.

Представлено много других ссылок на Википедию и неканонические источники. Все из которых я нахожу неактуальным. C ++ определяется стандартом .

Вывод:

Я готов признать, что есть много частей стандарта, которые я, возможно, не учел и могу доказать, что мои приведенные выше аргументы неверны. НЕ указаны ниже. Если вы покажете мне стандартную ссылку, которая показывает, что это UB. Я буду

  1. Оставьте ответ.
  2. Вставить все заглавные буквы, это глупо, и я не прав для всех читать.

Это не аргумент:

Не все во всем мире определяется стандартом C ++. Открой свой разум.

2 голосов
/ 12 июня 2009

Рабочий проект ( n2798 ):

"Результат унарного оператора & указатель на его операнд. Операнд должен быть lvalue или квалифицированным идентификатором. В первом случае, если тип выражение «Т», тип результат «указатель на T.» »(стр. 103)

array [5] не является квалифицированным идентификатором, насколько я могу судить (список на стр. 87); самым близким может показаться идентификатор, но пока массив является идентификатором, массив [5] - нет. Это не lvalue, потому что «lvalue относится к объекту или функции» (стр. 76). массив [5], очевидно, не является функцией, и не гарантируется, что он ссылается на действительный объект (поскольку массив + 5 находится после последнего выделенного элемента массива).

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

Примечание. Допустимо добавлять, чтобы обойти массив (стр. 113):

"если выражение P [указатель] указывает на последний элемент массива объект, выражение (P) +1 баллов один за последним элементом массива объект, и если выражение Q указывает один за последним элементом массива объект, выражение (Q) -1 указывает на последний элемент объекта массива. Если и операнд указателя, и результат указывают на элементы одного и того же объект массива или один за последним элемент массива объекта, оценка не должна давать над поток "

Но это недопустимо, используя &.

2 голосов
/ 12 июня 2009

Даже если это законно, зачем отходить от конвенции? массив + 5 в любом случае короче и, на мой взгляд, более читабелен.

Редактировать: если вы хотите симметрично, вы можете написать

int* array_begin = array; 
int* array_end = array + 5;
1 голос
/ 16 декабря 2013

Это должно быть неопределенное поведение по следующим причинам:

  1. Попытка получить доступ к элементам за пределами приводит к неопределенному поведению. Следовательно, стандарт не запрещает реализацию, генерирующую исключение в этом случае (то есть реализацию проверки границ до доступа к элементу). Если & (array[size]) было определено как begin (array) + size, реализация, генерирующая исключение в случае доступа за пределы границ, больше не будет соответствовать стандарту.

  2. Невозможно сделать этот выход end (array), если массив не массив, а произвольный тип коллекции.

...