Является ли неопределенным поведением формирование диапазона указателей из адреса стека?

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

Но это неопределенное поведение для формирования диапазона указателей из одной переменной стека, например:

char c = 'X';
char* begin = &c;
char* end = begin + 1;

for (; begin != end; ++begin) { /* do something */ }

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

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


person Channel72    schedule 02.02.2012    source источник
comment
Вы уверены, что правильно сформулировали, что сохранение адреса недопустимого указателя является поведением undefined? int* ptr; int** ptr2 = &ptr хранит адрес недопустимого указателя. Это УБ? И если вы имеете в виду, что у нас не может быть указателей на недействительную память, то откуда у нас могут быть указатели на NULL?   -  person Seth Carnegie    schedule 02.02.2012
comment
См. соответствующие stackoverflow.com/questions/8379186/   -  person ugoren    schedule 02.02.2012
comment
@SethCarnegie: int* ptr; int* ptr2 = &ptr даже не компилируется, потому что тип ptr2 не совпадает. Кроме того, nullptr является особым случаем.   -  person Mankarse    schedule 02.02.2012
comment
@Seth, NULL — это специальное значение, зарезервированное стандартом.   -  person Channel72    schedule 02.02.2012
comment
Channel72, который ответил только на один из моих вопросов. @Mankarse исправлено   -  person Seth Carnegie    schedule 02.02.2012
comment
@Сет, я не вижу проблемы. int** ptr2 = &ptr не указывает на недопустимый адрес. Он указывает на переменную указателя, выделенную в стеке.   -  person Channel72    schedule 02.02.2012
comment
@ Сет, на самом деле я понимаю, что ты имеешь в виду. Я думаю, вы были сбиты с толку моей формулировкой - я не должен был говорить о сохранении адреса недопустимого указателя, а скорее о сохранении недопустимого указателя.   -  person Channel72    schedule 02.02.2012
comment
Хорошо, спасибо, а также, кажется, что ответ на вопрос, который вы связали, указывает только на то, что выполнение арифметики с недопустимыми указателями - это UB. Например, int* ptr = (int*)0x12345678; UB? (не уверен, что гипс нужен)   -  person Seth Carnegie    schedule 02.02.2012
comment
Неопределенное поведение считывает значение недопустимого указателя, ничего не сохраняя. Это преобразование lvalue в rvalue вызывает неопределенное поведение, поэтому даже что-то вроде ptr == 0 является неопределенным поведением, если ptr является недопустимым указателем.   -  person James Kanze    schedule 02.02.2012
comment
@SethCarnegie Актерский состав явно необходим. А любое использование полученного в результате case значения (включая его копирование в именованную переменную) — поведение undefined. В соответствии со стандартом --- реализация может определить его, если захочет.   -  person James Kanze    schedule 02.02.2012
comment
@James Какую часть стандарта вы имеете в виду, когда говорите это?   -  person Seth Carnegie    schedule 02.02.2012
comment
Тот факт, что стандарт не определяет, что происходит. Это (потенциально) недопустимый указатель, а преобразование lvalue в rvalue любого недопустимого объекта является поведением undefined. (За одним исключением: типы символов.)   -  person James Kanze    schedule 02.02.2012
comment
@JamesKanze: союзы и типы структур во многих случаях являются еще одним исключением; учитывая struct { int *p; } foo,bar;, оператор bar = foo; может быть безопасно вызван независимо от того, содержит ли foo действительный указатель. Если foo содержит недопустимый указатель, присваивание приведет к тому, что bar сделает то же самое, а попытка использовать bar.p вызовет неопределенное поведение. Насколько я понимаю, могут быть некоторые разногласия по поводу того, потребуется ли foo = bar; копировать что-либо в случае, если bar известно как недействительное; Я думаю, что это необходимо сделать, если какой-либо код может...   -  person supercat    schedule 04.07.2015
comment
... используйте memcmp для сравнения foo с указателем, время жизни которого перекрывает время жизни bar.p, но я не думаю, что это представление является общедоступным.   -  person supercat    schedule 04.07.2015


Ответы (6)


Это разрешено, поведение определено, а begin и end являются значениями безопасных производных указателей.

В стандартном разделе С++ 5.7 ([expr.add]), пункт 4:

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

При использовании C аналогичный пункт можно найти в параграфе 7 раздела 6.5.6 стандарта C99/N1256.

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


Кстати, в разделе 3.7.4.3 ([basic.stc.dynamic.safety]) "Безопасно полученные указатели" есть сноска:

Этот раздел не накладывает ограничений на разыменование указателей на память, не выделенную ::operator new. Это позволяет многим реализациям C++ использовать двоичные библиотеки и компоненты, написанные на других языках. В частности, это относится к двоичным файлам C, поскольку разыменование указателей на память, выделенную malloc, не ограничено.

Это говорит о том, что арифметика указателей по всему стеку является поведением, определяемым реализацией, а не неопределенным поведением.

person Ben Voigt    schedule 02.02.2012
comment
Здесь много противоречивых ответов и интерпретаций, но эти ответы кажутся довольно окончательными. - person Channel72; 02.02.2012
comment
Поскольку этот вопрос также был отмечен тегом C, такое же поведение указано в спецификации C99 (N1256) в разделе 6.5.6, параграф 7. - person tinman; 02.02.2012
comment
Спасибо, @tinman. Если у вас есть реальный текст, не стесняйтесь добавлять цитату. - person Ben Voigt; 02.02.2012
comment
Параграф [expr.add], по-видимому, применяется только к аддитивным операторам. Я не могу найти соответствующий абзац для реляционных операторов. - person Mankarse; 01.03.2012
comment
@Mankarse: end = begin + 1... мне кажется, что это аддитивный оператор. - person Ben Voigt; 01.03.2012
comment
@BenVoigt: Конечно, но алгоритм, использующий RandomAccessIterators, может законно оценить begin < end, поэтому в целом результирующий диапазон будет небезопасно передавать в алгоритм, ожидающий RandomAccessIterators. - person Mankarse; 02.03.2012
comment
@Mankarse: вопрос интерпретации. Я бы сказал, что если добавление ведет себя одинаково, то выражение добавления указателя ДОЛЖНО оценивать указатель, который сравнивается больше, чем оригинал, потому что это происходит при добавлении указателя в массив. - person Ben Voigt; 02.03.2012
comment
@BenVoigt - Верно, и трудно понять, как может быть полезно разрешить создание диапазона указателей для одного объекта без результирующих указателей, действующих полностью как указатели на элементы массива. - person Mankarse; 02.03.2012

Я считаю, что по закону вы можете рассматривать один объект как массив единичного размера. Кроме того, совершенно определенно допустимо брать указатель после конца любого массива, если он не разыменован. Так что я считаю, что это не УБ.

person Puppy    schedule 02.02.2012

Это не неопределенное поведение, если вы не разыменовываете недействительный итератор.
Вам разрешено удерживать указатель на память за пределами вашего выделения, но не разрешается разыменовывать ее.

person Alok Save    schedule 02.02.2012
comment
Вопрос, на который я ссылаюсь, указывает, что вам НЕ разрешено удерживать указатель на адрес за пределами вашего выделения (что удивляет многих людей), если только он не находится за концом массива. - person Channel72; 02.02.2012
comment
который этот. Поэтому это определено здесь. - person CashCow; 02.02.2012

5.7-5 ISO14882:2011(e) гласит:

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

Если я что-то там не упустил, дополнение применяется только к указателям, указывающим на один и тот же массив. Для всего остального применяется последнее предложение: «иначе поведение не определено».

редактировать: Действительно, когда вы добавляете 5.7-4, оказывается, что операция, которую вы выполняете, (виртуально) выполняется над массивом, поэтому предложение не применяется:

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

person PlasmaHH    schedule 02.02.2012
comment
Вы что-то упустили из виду. Смотрите мой ответ. Или ДэдМГ. - person Ben Voigt; 02.02.2012

В общем случае было бы неопределенным поведением указывать за пределы пространства памяти, однако есть исключение для «один за концом», которое допустимо в соответствии со стандартом.

Поэтому в конкретном примере &c+1 является допустимым указателем, но его нельзя безопасно разыменовать.

person CashCow    schedule 02.02.2012

Вы можете определить c как массив размера 1:

char c[1] = { 'X' };

Тогда неопределенное поведение станет определенным поведением. Результирующий код должен быть идентичным.

person unknownfrog    schedule 02.02.2012
comment
Вы могли бы, но 1. в этом нет необходимости и 2. это не вопрос пользователя. Здесь нет неопределенного поведения, и ваш ответ может получить несколько отрицательных голосов, хотя я сам его не давал. - person CashCow; 02.02.2012
comment
Начнем с того, что это не неопределенное поведение. - person Ben Voigt; 02.02.2012
comment
Если бы исходный код был неопределенным поведением, то этот код был бы определенным поведением. Подобно переполнению целых чисел со знаком, это поведение undefined (и некоторые компиляторы используют это для некоторой оптимизации). Там вы можете использовать приведение от подписанного к неподписанному типу, чтобы получить определенное поведение. - person unknownfrog; 02.02.2012