Чи завжди покажчик із правильною адресою та типом завжди є дійсним покажчиком з C ++ 17?


84

(Стосовно цього питання та відповіді .)

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

Якщо об’єкт типу T знаходиться за адресою A, вказівник типу cv T *, значення якого є адресою A, вказує на цей об’єкт, незалежно від того, як було отримано значення.

Але з C ++ 17 це речення було вилучено .

Наприклад, я вважаю, що це речення визначило цей приклад коду, і що з C ++ 17 це невизначена поведінка:

 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) Який компілятор розміщує var у реєстрі після того, як ви взяли його адресу? 2) Як ви правильно вгадуєте адресу, не вимірюючи її?
curiousguy

@curiousguy Саме тому, просто кинути число, отримане іншими способами (наприклад, вгадуванням) на адресу, де знаходиться об'єкт, є проблематичним: це псевдонім цього об'єкта, але компілятор цього не знає. Якщо ви, навпаки, берете адресу об’єкта, вона така, як ви говорите: компілятор отримує попередження та синхронізується відповідно.
Пітер - відновити Моніку

Відповіді:


45

Це тлумачення цієї модифікації стандартного права чи існують інші правила, що компенсують видалення цього цитованого речення?

Так, це тлумачення є правильним. Вказівник минулого кінця не просто конвертується в інше значення покажчика, яке вказує на цю адресу.

У новому [basic.compound] / 3 сказано:

Кожне значення типу покажчика є одним із наступних:
(3.1) вказівник на об'єкт або функцію (вказівник, як кажуть, вказує на об'єкт або функцію), або
(3.2) вказівник за кінцем об'єкта ([вираз .add]), або

Вони взаємовиключні. p1+1- це вказівник за кінцем, а не вказівник на об’єкт. p1+1вказує на гіпотетичний x[1]масив розміру 1 p1, а не на p2. Ці два об'єкти не можуть бути перетворюваними між покажчиками.

У нас також є ненормативна примітка:

[Примітка: Вказівник, що минає кінець об’єкта ([expr.add]), не вважається таким, що вказує на непов’язаний об’єкт типу об’єкта, який може бути розташований за цією адресою. [...]

що уточнює намір.


Як зазначає ТК у численних коментарях ( зокрема, цьому ), це насправді особливий випадок проблеми, яка виникає при спробі реалізації 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 у версії до C ++ 17?
Олів,

1
@Oliv Якщо арифметика покажчика фіксована, тоді ви p1+1більше не створюватимете вказівник минулого кінця, і вся дискусія про вказівники минулого кінця спірна. Ваш конкретний двоелементний особливий випадок може бути не UB до 17, але це також не дуже цікаво.
TC

5
@TC Чи можете ви десь вказати мені, що я можу прочитати цю "проблему реалізації вектора"?
SirGuy

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 [ n]. Представлення значень типів покажчиків визначається реалізацією. Вказівники на типи, сумісні з макетом, повинні мати однакові вимоги щодо подання значення та вирівнювання (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

@StoryTeller: Я теж сподіваюся. До того ж ні слова про це в розділі сумісності. Але, схоже, тут протилежна думка має більшу репутацію ...
Серж Баллеста

2
Ви захоплюєте одне слово, "не пов’язане", у ненормативній примітці і надаєте йому значення, яке воно не може нести, всупереч нормативним правилам [expr.add], що регулюють арифметику покажчика. У Додатку С немає нічого, оскільки арифметика покажчика загального випадку ніколи не працювала за жодним стандартом. Ламати нічого.
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]". Це не може бути добре, і, безумовно, ніколи не має наміру.


Чи ефективно це також забороняє використання порожнього масиву в кінці структури таким чином, що похідний клас або спеціальний розподільник new може вказувати масив нестандартного розміру? Можливо, новий випуск стосується питання "незалежно від того, як" - є деякі способи, які є дійсними, а деякі небезпечні?
Gem Taylor

@Persixty Отже, значення об’єкта вказівника визначається байтами об’єктів, і нічим іншим. Отже, два об’єкти з однаковим станом вказують на один і той же об’єкт. Якщо один дійсний, то інший теж. Отже, на загальноприйнятих архітектурах, де значення покажчика представлене як число, два вказівники з однаковими значеннями вказують на однакові об’єкти, а один із кінцевих - на ті самі інші об’єкти.
curiousguy

@Persixty Крім того, тривіальний тип означає, що ви можете перерахувати можливі значення типу. По суті, будь-який сучасний компілятор в будь-якому режимі оптимізації (навіть -O0на деяких компіляторах) не розглядає покажчики як тривіальні типи. Укладачі не ставляться до вимог std серйозно, як і до людей, які пишуть std, які мріють про іншу мову та роблять всілякі винаходи, що прямо суперечать основним принципам. Очевидно, що користувачі розгублені і іноді погано поводяться з ними, коли вони скаржаться на помилки компілятора.
curiousguy

Ненормативна примітка у питанні вимагає, щоб ми думали про "минуле-кінець" як про те, що ні на що не вказує. Ми обидва знаємо, що на практиці це цілком може вказувати на щось, і на практиці це може бути можливо відмінити. Але це (згідно зі стандартом) не є допустимою програмою. Ми можемо уявити реалізацію, яка знає, що покажчик був отриманий за допомогою арифметики-минулого-кінця і викликає виняток, якщо його не визначено. Хоча я знаю відому платформу, яка це робить. Я думаю, що стандарт не хоче його виключати.
Persixty

@curiousguy Крім того, я не впевнений, що ви маєте на увазі, перераховуючи можливі значення. Це не є обов’язковою функцією тривіального типу, як це визначено в C ++.
Persixty
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.