Всегда ли указатель с правильным адресом и типом является действительным указателем, начиная с C ++ 17?

84

(В отношении этого вопроса и ответа .)

До стандарта C ++ 17 в [basic.compound] / 3 было включено следующее предложение :

Если объект типа T расположен по адресу A, говорят, что указатель типа cv T *, значением которого является адрес A, указывает на этот объект, независимо от того, как было получено значение.

Но начиная с C ++ 17 это предложение было удалено .

Например, я считаю, что это предложение определило этот пример кода, и что, начиная с C ++ 17, это поведение undefined:

 alignas(int) unsigned char buffer[2*sizeof(int)];
 auto p1=new(buffer) int{};
 auto p2=new(p1+1) int{};
 *(p1+1)=10;

До C ++ 17 p1+1содержал адрес *p2и имел правильный тип, так *(p1+1)же как и указатель на *p2. В C ++ 17 p1+1это указатель за концом , поэтому он не является указателем на объект, и я считаю, что его нельзя разыменовать.

Это интерпретация данной модификации стандартного права или существуют другие правила, которые компенсируют удаление процитированного предложения?

Олив
источник
Примечание: есть новые / обновленные правила о происхождении указателя в [basic.stc.dynamic.safety] и [util.dynamic.safety]
MM
@MM Это имеет значение только для реализаций со строгой безопасностью указателя, который представляет собой пустой набор (с точностью до экспериментальной ошибки).
TC
4
Цитируемое заявление никогда не было правдой на практике. Учитывая int a, b = 0;, вы не можете этого сделать, *(&a + 1) = 1;даже если вы проверили &a + 1 == &b. Если вы можете получить действительный указатель на объект, просто угадав его адрес, то даже сохранение локальных переменных в регистрах становится проблематичным.
TC
@TC 1) Какой компилятор помещает переменную в reg после того, как вы взяли ее адрес? 2) Как правильно угадать адрес, не измерив его?
curiousguy 04
@curiousguy Именно поэтому простое приведение числа, полученного другими способами (например, угадыванием) к адресу, где находится объект, является проблематичным: это псевдоним этого объекта, но компилятор не знает об этом. Если вы, напротив, возьмете адрес объекта, он будет таким, как вы говорите: компилятор получает предупреждение и синхронизируется соответственно.
Питер - Восстановить Монику

Ответы:

45

Это интерпретация данной модификации стандартного права или есть другие правила, которые компенсируют удаление этого процитированного предложения?

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

В новом [basic.compound] / 3 говорится:

Каждое значение типа указателя является одним из следующих:
(3.1) указатель на объект или функцию (говорят, что указатель указывает на объект или функцию) или
(3.2) указатель за концом объекта ([expr .add]) или

Это взаимоисключающие. p1+1является указателем за концом, а не указателем на объект. p1+1указывает на гипотетический x[1]массив размера 1 на p1, а не на p2. Эти два объекта не взаимопреобразуемы по указателям.

У нас также есть ненормативное примечание:

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

что проясняет намерение.


Как указывает TC в многочисленных комментариях (в частности, в этом ), это действительно частный случай проблемы, которая возникает при попытке реализовать std::vector- это [v.data(), v.data() + v.size())должен быть допустимый диапазон и при этом vectorне создается объект массива, поэтому только арифметика с определенным указателем будет переходить от любого заданного объекта в векторе до конца его гипотетического одноразмерного массива. Для получения дополнительных ресурсов см. CWG 2182 , это стандартное обсуждение и две версии документа по этому вопросу: P0593R0 и P0593R1 (в частности, раздел 1.3).

Барри
источник
3
Этот пример, по сути, является частным случаем известной « vectorпроблемы реализуемости». +1.
TC
2
@Oliv Общий случай существует с C ++ 03. Основная причина - арифметика указателей не работает должным образом, потому что у вас нет объекта массива.
TC
1
@TC Я полагал, что единственная проблема связана с ограничением арифметики указателей. Разве это удаление предложения не добавляет новой проблемы? Является ли пример кода также UB в pre-C ++ 17?
Oliv
1
@Oliv Если указатель арифметик фиксирован, то ваш p1+1больше не будет производить пришедшие к торцевому указателю и вся дискуссия о пришедших к торцевым указателям спорна. Ваш конкретный двухэлементный особый случай может не быть UB до 17, но это тоже не очень интересно.
TC
5
@TC Можете ли вы указать мне где-нибудь, чтобы я мог прочитать об этой "проблеме реализуемости векторов"?
SirGuy 02
8

В вашем примере это *(p1 + 1) = 10;должно быть UB, потому что это один за концом массива размера 1. Но мы находимся в очень особом случае здесь, потому что массив был динамически построен в большем массиве char.

Создание динамических объектов описано в 4.5 Объектная модель C ++ [intro.object] , §3 черновика n4659 стандарта C ++:

3 Если в хранилище создается полный объект (8.3.4), связанный с другим объектом e типа «массив из N символов без знака» или типа «массив из N std :: byte» (21.2.1), этот массив обеспечивает хранилище для созданного объекта, если:
(3.1) - время жизни e началось и не закончилось, и
(3.2) - хранилище для нового объекта полностью помещается в e, и
(3.3) - нет меньшего объекта массива, который удовлетворяет этим ограничения.

Версия 3.3 кажется довольно неясной, но примеры ниже проясняют замысел:

struct A { unsigned char a[32]; };
struct B { unsigned char b[16]; };
A a;
B *b = new (a.a + 8) B; // a.a provides storage for *b
int *p = new (b->b + 4) int; // b->b provides storage for *p
// a.a does not provide storage for *p (directly),
// but *p is nested within a (see below)

Итак, в этом примере bufferмассив предоставляет хранилище для *p1и *p2.

Следующие пункты доказывают , что законченным объектом для обоих *p1и *p2является buffer:

4 Объект a вложен в другой объект b, если:
(4.1) - a является подобъектом b, или
(4.2) - b обеспечивает хранилище для a, или
(4.3) - существует объект c, где a вложено в c , а c вложен в b.

5 Для каждого объекта x существует некоторый объект, называемый полным объектом x, который определяется следующим образом:
(5.1) - Если x является полным объектом, то полный объект x является самим собой.
(5.2) - В противном случае полный объект x является полным объектом (уникального) объекта, который содержит x.

Как только это будет установлено, другой соответствующей частью проекта n4659 для C ++ 17 будет [basic.coumpound] §3 (выделите мой):

3 ... Каждое значение типа указателя является одним из следующих:
(3.1) - указатель на объект или функцию (говорят, что указатель указывает на объект или функцию), или
(3.2) - указатель за концом объекта (8.7), или
(3.3) - значение нулевого указателя (7.11) для этого типа, или
(3.4) - недопустимое значение указателя.

Значение типа указателя, которое является указателем на конец объекта или за ним, представляет адрес первого байта в памяти (4.4), занятого объектом, или первого байта в памяти после конца памяти, занятой объектом. соответственно. [Примечание: указатель за концом объекта (8.7) не считается указывающим на несвязанныйобъект типа объекта, который может находиться по этому адресу. Значение указателя становится недействительным, когда память, которую он обозначает, достигает конца срока хранения; см. 6.7. - конец примечания] В целях арифметики указателей (8.7) и сравнения (8.9, 8.10) указатель за концом последнего элемента массива x из n элементов считается эквивалентным указателю на гипотетический элемент x [ п]. Представление значений типов указателей определяется реализацией. Указатели на типы, совместимые с макетом, должны иметь одинаковые требования к представлению значений и выравниванию (6.11) ...

Примечание Указатель за концом ... здесь не применяется, потому что объекты, на которые указывает p1и p2не являются несвязанными , но вложены в один и тот же полный объект, поэтому арифметика указателей имеет смысл внутри объекта, который обеспечивает хранилище: p2 - p1определен и является (&buffer[sizeof(int)] - buffer]) / sizeof(int)то есть 1.

Так p1 + 1 есть указатель *p2, и *(p1 + 1) = 10;имеет определенное поведение и устанавливает значение *p2.


Я также прочитал приложение C4 о совместимости между C ++ 14 и текущими стандартами (C ++ 17). Удаление возможности использовать арифметику указателей между объектами, динамически создаваемыми в едином символьном массиве, было бы важным изменением, на которое следует упомянуть IMHO, потому что это часто используемая функция. Поскольку на страницах совместимости ничего об этом не содержится, я думаю, что это подтверждает, что стандарт не имел намерения запрещать это.

В частности, это нарушило бы обычное динамическое построение массива объектов из класса без конструктора по умолчанию:

class T {
    ...
    public T(U initialization) {
        ...
    }
};
...
unsigned char *mem = new unsigned char[N * sizeof(T)];
T * arr = reinterpret_cast<T*>(mem); // See the array as an array of N T
for (i=0; i<N; i++) {
    U u(...);
    new(arr + i) T(u);
}

arr затем можно использовать как указатель на первый элемент массива ...

Серж Бальеста
источник
Ага, так мир не сошел с ума. +1
StoryTeller - Unslander Monica 02
@StoryTeller: Я тоже надеюсь. К тому же ни слова об этом в разделе совместимости. Но похоже, что здесь больше репутации противоположного мнения ...
Серж Баллеста
2
Вы берете одно слово «несвязанный» в ненормативном примечании и придаете ему значение, которое оно не может иметь, в нарушение нормативных правил [expr.add], управляющих арифметикой указателей. В Приложении C ничего нет, потому что арифметика указателей общего случая никогда не работала ни в одном стандарте. Нечего ломать.
TC
3
@TC: Google очень бесполезен в поиске какой-либо информации об этой «проблеме реализуемости векторов», не могли бы вы помочь?
Matthieu M.
6
@MatthieuM. См основной вопрос 2182 , этот станд-нить обсуждения, Основную ветку стандартного P0593R0 и P0593R1 (особенно раздел 1.3) . Основная проблема заключается в том, что vectorне создается (и не может быть) объект массива, но есть интерфейс, который позволяет пользователю получить указатель, поддерживающий арифметику указателей (которая определена только для указателей на объекты массива).
TC
1

Чтобы расширить приведенные здесь ответы, приведу пример того, что, по моему мнению, исключает измененная формулировка:

Предупреждение: неопределенное поведение

#include <iostream>
int main() {
    int A[1]{7};
    int B[1]{10};
    bool same{(B)==(A+1)};

    std::cout<<B<< ' '<< A <<' '<<sizeof(*A)<<'\n';
    std::cout<<(same?"same":"not same")<<'\n';
    std::cout<<*(A+1)<<'\n';//!!!!!  
    return 0;
}

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

0x7fff1e4f2a64 0x7fff1e4f2a60 4
same
10

Эти выходные данные показывают, что два массива (в этом случае) хранятся в памяти так, что «один за концом» хранит Aзначение адреса первого элемента B.

Пересмотренная спецификация гарантирует, что независимо A+1никогда не будет действительным указателем на B. Старая фраза «независимо от того, как получено значение» гласит, что если «A + 1» указывает на «B [0]», то это действительный указатель на «B [0]». Это не может быть добром, и уж точно не намерением.

Persixty
источник
Эффективно ли это запрещает использование пустого массива в конце структуры, чтобы производный класс или настраиваемый распределитель new мог указывать массив нестандартного размера? Возможно, новая проблема связана с «независимо от того, как» - есть некоторые способы, которые действительны, а некоторые способы, которые опасны?
Джем Тейлор
@Persixty Значит, значение объекта-указателя определяется байтами объектов и ничем другим. Итак, два объекта с одинаковым состоянием указывают на один и тот же объект. Если один действителен, другой тоже. Таким образом, в обычных архитектурах, где значение указателя представлено числом, два указателя с равными значениями указывают на одни и те же объекты, а один из концов - на те же другие объекты.
curiousguy 05
@Persixty Также тривиальный тип означает, что вы можете перечислить возможные значения типа. По сути, любой современный компилятор в любом режиме оптимизации (даже -O0в некоторых компиляторах) не рассматривает указатели как тривиальные типы. Компиляторы не относятся серьезно к требованиям std, равно как и люди, которые пишут std, мечтают о другом языке и делают всевозможные изобретения, прямо противоречащие основным принципам. Очевидно, что пользователи сбиты с толку и иногда плохо обращаются, когда жалуются на ошибки компилятора.
curiousguy 05
Ненормативное примечание в вопросе заставляет нас думать, что «один за концом» ни на что не указывает. Мы оба знаем на практике, что это вполне может указывать на что-то, и на практике это возможно разыменовать. Но это (согласно стандарту) неверная программа. Мы можем представить себе реализацию, которая знает, что указатель был получен путем арифметики до конца, и вызывает исключение при разыменовании. Хотя я знаю платформу, которая это делает. Я думаю, что стандарт не хочет этого исключать.
Persixty 05
@curiousguy Кроме того, я не уверен, что вы имеете в виду, перечисляя возможные значения. Это не обязательная особенность тривиального типа, как определено в C ++.
Persixty 05