Получение указателя за концом с использованием адреса массива

В C и C++ часто полезно использовать указатель за концом для написания функций, которые могут работать с произвольно большими массивами. C++ предоставляет перегрузку std::end, чтобы упростить эту задачу. С другой стороны, в C я обнаружил, что нередко можно увидеть макрос, определенный и используемый следующим образом:

#define ARRAYLEN(array) (sizeof(array)/sizeof(array[0]))

// ...

int a [42];
do_something (a, a + ARRAYLEN (a));

Я также видел арифметический трюк с указателями, позволяющий таким функциям работать с отдельными объектами:

int b;
do_something (&b, &b + 1);

Мне пришло в голову, что что-то подобное можно сделать и с массивами, поскольку они рассматриваются C (и, я думаю, C++) как «полные объекты». Получив массив, мы можем получить указатель на массив сразу после него, разыменовать этот указатель и использовать преобразование массива в указатель для результирующей ссылки на массив, чтобы получить указатель за концом для исходного массива:

#define END(array) (*(&array + 1))

// ...

int a [42];
do_something (a, END (a));

Мой вопрос заключается в следующем: При разыменовывании указателя на несуществующий объект массива этот код демонстрирует неопределенное поведение? Меня интересует, что говорят об этом самые последние версии C и C++? этот код (не потому, что я собираюсь его использовать, так как есть лучшие способы достижения того же результата, а потому, что это интересный вопрос).


person Stuart Olsen    schedule 14.12.2013    source источник
comment
Я удивлен, что у этого еще нет ответа, но после большого количества чтения я думаю, что консенсус заключается в том, что указание одного за концом массива является допустимым указателем, который нельзя разыменовать. Обычный отрывок из стандарта C, который цитируется, это 6.5.6/8. В С++ это 5.7/5. Если вам интересно, вот ссылка на средство проверки различий.   -  person    schedule 14.12.2013
comment
@remyabel Похоже, это указывает на то, что код незаконен. В целях арифметики указателей C (не уверен в C++) считает полный объект эквивалентным единственному элементу массива степени 1 (в этом случае массив типа int[42] является единственным элементом массива типа int[1][42]). 6.5.8 явно запрещает разыменование указателя за концом (в оцениваемом контексте), такого как указатель, образованный &array + 1, который разыменовывается.   -  person Stuart Olsen    schedule 14.12.2013
comment
@StuartOlsen: Но является ли преобразование массива в указатель (обычно называемое распадом) оцененным контекстом? Он не использует значение объекта, а только его адрес.   -  person Ben Voigt    schedule 14.12.2013
comment
@BenVoigt Я считаю, что правило оцененного контекста относится к оцениваемой косвенности (т. Е. *ptr появляется вне выражения sizeof/alignof/_Alignof). Если результат указывает на один после последнего элемента объекта массива, он не должен использоваться в качестве операнда оцениваемого унарного оператора * - 6.5.6.8, N1570.   -  person Stuart Olsen    schedule 14.12.2013
comment
@StuartOlsen: Это, несомненно, неоцененные контексты. Но могут быть и другие. В C++, например, привязка ссылки не оценивает объект, к которому она привязана. (Примечание к пункту 1 раздела 5.3.1)   -  person Ben Voigt    schedule 14.12.2013
comment
@BenVoigt Верно, но я думаю, что правило говорит об оцениваемом операторе косвенности, а не об оцениваемом результирующем значении lvalue. Другими словами, если я правильно понимаю текст, даже int* p = NULL; *p; недействителен, по крайней мере, в C.   -  person Stuart Olsen    schedule 14.12.2013
comment
@BenVoigt Да, плохой пример. Лучше было бы int a [1]; a [1]; 6.5.6.8 в C определенно делает это незаконным, независимо от отсутствия преобразования lvalue-rvalue. Ради согласованности это должно сделать *NULL незаконным, но если UB для унарного * вообще определяется преобразованием lvalue-rvalue, я не уверен, что это так, потому что в этом выражении такого преобразования нет. Однако это выходит за рамки вопроса.   -  person Stuart Olsen    schedule 14.12.2013
comment
@Stuart: Джерри Коффин нашел этот отчет о дефекте в котором явно упоминается случай *p; и говорится, что он не запускает преобразование в rvalue, поэтому нет неопределенного поведения. Так что мой предыдущий комментарий был ошибочным. (Тогда не уверен, как это работает, чтобы принудительно читать через изменчивый указатель.)   -  person Ben Voigt    schedule 14.12.2013
comment
@BenVoigt Насколько я читал, предлагаемое решение определенно сделало бы код допустимым на C ++, поскольку (&array[1]) создавало бы так называемое пустое lvalue, а преобразование массива в указатель не вызывало бы предложение UB. К сожалению, похоже, что он еще не вышел из стадии разработки; N3797 не включает предложенную формулировку.   -  person Stuart Olsen    schedule 14.12.2013
comment
@Stuart: Нет, но комментарии, касающиеся извлечения языка из C (&*p не является операцией), заявляют, что C ++ уже обрабатывает это, потому что создание lvalue путем разыменования недопустимого указателя не является UB, только преобразование lvalue-rvalue для таких lvalue есть.   -  person Ben Voigt    schedule 14.12.2013


Ответы (3)


Я использовал это в своем собственном коде как (&arr)[1].

Я совершенно уверен, что это безопасно. Преобразование массива в указатель не является «преобразованием lvalue в rvalue», хотя оно начинается с lvalue и заканчивается rvalue.

person Ben Voigt    schedule 14.12.2013
comment
Разрешает ли какой-либо стандарт косвенно использовать указатель, который не указывает на объект (например, указатель &arr + 1), если результирующее значение lvalue (ссылка) не подвергается преобразованию из lvalue в rvalue? Оба языка ссылаются на объект, на который ссылается указатель, что, по-видимому, подразумевает, что должен быть объект соответствующего типа в месте, указанном указателем, чтобы разыменовать его. Лучшее, что я могу найти, это то, что C допускает такую ​​косвенность при немедленной отмене оператором адреса (например, &*NULL), что не совсем то, что здесь происходит. - person Stuart Olsen; 14.12.2013
comment
@Stuart: Стандарт C ++ содержит (4p8) примечание на этот счет и делает операнд оператора адреса & примером ... но он не ограничивается оператором &, он применяется везде, где lvalue-to-rvalue конверсия не появляется. Кроме того, правило, которое делает доступ через недопустимое значение указателя неопределенным поведением, — это 4.1p2, и оно применяется только к преобразованию lvalue в rvalue. - person Ben Voigt; 14.12.2013

Это неопределенное поведение.

a относится к типу array of 42 int.

&a относится к типу pointer to array of 42 int. (Обратите внимание, что это не преобразование массива в указатель)

&a + 1 также относится к типу pointer to array of 42 int

5.7p5 гласит:

Когда выражение, имеющее целочисленный тип, добавляется к указателю или вычитается из него, результат имеет тип операнда указателя. Если операнд указателя указывает на элемент объекта массива, а [...] в противном случае поведение не определено

Указатель не указывает на элемент объекта массива. Он указывает на объект массива. Таким образом, «в противном случае поведение не определено» верно. Поведение неопределенное.

person Andrew Tomazos    schedule 14.12.2013
comment
По крайней мере, в C++ это неверно. Существует правило, позволяющее рассматривать каждый объект как массив размера 1. - person Ben Voigt; 14.12.2013
comment
@BenVoigt: стандартная ссылка? - person Andrew Tomazos; 14.12.2013
comment
5.7p4 Для целей этих операторов указатель на объект, не являющийся массивом, ведет себя так же, как указатель на первый элемент массива длины один с типом объекта в качестве типа его элемента. - person Ben Voigt; 14.12.2013
comment
@BenVoigt: это указатель на объект массива, поэтому 5.7p4 не применяется. Хотя я склонен считать это недостатком. Я думаю, что 5.7p4 должен читать..., указатель, который не является указателем на элемент массива... вместо..., указатель на объект, не являющийся массивом... - person Andrew Tomazos; 14.12.2013
comment
Да, очевидно, что целью является поведение, которое я дал в своем комментарии при перефразировании. Или, возможно, указатель на объект массива также считается указателем на его первый элемент, что делает арифметику указателя допустимой. - person Ben Voigt; 14.12.2013
comment
@AndrewTomazos В проекте N1570 для C говорится, что указатель на объект, который не является элементом массива, ведет себя так же, как указатель на первый элемент массива длины один с типом объекта в качестве его типа элемента. Странно, что C++ использует другое правило. - person Stuart Olsen; 14.12.2013
comment
О дефекте сообщается в ISO здесь: группы. google.com/a/isocpp.org/forum/?fromgroups#!topic/ - person Andrew Tomazos; 14.12.2013
comment
@AndrewTomazos Этот форум не является официальным каналом; отчет о дефекте не отправляется до тех пор, пока не будет отправлен по электронной почте сопровождающему, указанному в последнем списке аварийного восстановления, в настоящее время [email protected] . - person Potatoswatter; 14.12.2013
comment
@Potatoswatter: Есть ли где-нибудь инструкции по форматированию электронной почты? - person Andrew Tomazos; 14.12.2013
comment
@Potatoswatter: инструкции здесь isocpp.org/std/submit-a-library-issue сначала произнесите сообщение в std-обсуждении. Пожимайте плечами. - person Andrew Tomazos; 14.12.2013
comment
@AndrewTomazos К сожалению, этот процесс остается на усмотрение сопровождающего. По моему опыту, отчеты CWG лучше делать относительно краткими, чтобы не урезать их до нужного размера. - person Potatoswatter; 14.12.2013
comment
@AndrewTomazos Эти инструкции в основном предназначены для того, чтобы отсеять неязыковых юристов. Учитывая, что это уже было признано и исправлено в C, это должно быть достаточно бесспорным, чтобы сразу обратиться в комитет. - person Potatoswatter; 14.12.2013

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

Но основная идея использования &array + 1 верна, когда array является lvalue. (Бывают случаи, когда массивы не являются lvalue.) В этом случае это допустимая операция с указателем. Теперь, чтобы получить указатель на первый элемент, вам просто нужно вернуть его к базовому типу. В вашем случае это будет

(int*)(&array + 1)

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

К сожалению, я не вижу способа сделать такой тип выражения независимым, чтобы вы могли поместить его в общий макрос, если только вы не приведете к void*. (С расширением gcc typeof вы могли бы сделать, например) Так что вам лучше придерживаться портативного (array)+ARRAYLEN(array), он должен работать во всех случаях.

В странном угловом случае массив, который является частью struct и возвращается как rvalue из функции, не является lvalue. Я думаю, что стандарт допускает и здесь арифметику с указателями, но я никогда не понимал эту конструкцию полностью, поэтому не уверен, что она будет работать в этом случае.

person Jens Gustedt    schedule 14.12.2013