Чи приріст покажчика на динамічний масив розміром 0 не визначений?


34

AFAIK, хоча ми не можемо створити масив статичної пам'яті розміром 0, але ми можемо це зробити з динамічними:

int a[0]{}; // Compile-time error
int* p = new int[0]; // Is well-defined

Як я читав, він pдіє як елемент "минулого кінця". Я можу надрукувати адресу, на яку pвказує.

if(p)
    cout << p << endl;
  • Хоча я впевнений, що ми не можемо скинути цей покажчик (минулий-останній елемент), як ми не можемо з ітераторами (минулий-останній елемент), але в чому я не впевнений, чи збільшується цей покажчик p? Чи не визначена поведінка (UB), як у ітераторів?

    p++; // UB?

4
UB "... Будь-які інші ситуації (тобто спроби генерувати покажчик, який не вказує на елемент того ж масиву або один на кінець), викликають невизначене поведінку ...." з: en.cppreference.com / w / cpp / мова / operator_arithmetic
Річард Криттен

3
Ну, це схоже на std::vectorелемент із 0 пунктом. begin()вже дорівнює, end()тому ви не можете збільшувати ітератор, який вказує на початку.
Phil1970

1
@PeterMortensen Я думаю, що ваша редакція змінила значення останнього речення ("У чому я впевнений -> я не впевнений, чому").
Фабіо каже

@PeterMortensen: Останній редактор, який ви редагували, став трохи читабельнішим.
Ітачі Учіва

Відповіді:


32

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

Ваш масив розміром 0 pвже вказує один набік, тому збільшувати його не дозволяється.

Див. C ++ 17 8.7 / 4 щодо +оператора ( ++має ті самі обмеження):

f вираз Pвказує на елемент x[i]об’єкта масиву xз n елементами, вирази P + Jта J + P(де Jмає значення j) вказують на (можливо, гіпотетичний) елемент, x[i+j]якщо 0≤i + j≤n; в іншому випадку поведінка не визначена.


2
Тож єдиний випадок x[i]такий же, як x[i + j]і коли обидва iі jмають значення 0?
Рамі Єн

8
@RamiYen x[i]- той самий елемент, як x[i+j]ніби j==0.
interjay

1
Фу, я ненавиджу "зону сутінків" C ++ семантики ... хоча 1.
einpoklum

4
@ einpoklum-reinstateMonica: Зоні сутінків насправді немає. Це просто C ++, який є послідовним навіть для випадку N = 0. Для масиву з N елементів є N + 1 допустимих значень вказівника, оскільки ви можете вказати позаду масиву. Це означає, що ви можете почати з початку масиву і збільшити покажчик N разів, щоб дійти до кінця.
MSalters

1
@MaximEgorushkin Моя відповідь - про те, що дозволяє мова на даний момент. Обговорення про те, що ви хотіли б, щоб це дозволяло, натомість, поза темою.
interjay

2

Я думаю, у вас вже є відповідь; Якщо ви заглянете трохи глибше: Ви сказали, що збільшенням ітератора в кінці є UB таким чином: Ця відповідь у тому, що таке ітератор?

Ітератор - це просто об’єкт, який має вказівник і збільшує, що ітератор дійсно збільшує вказівник, який він має. Таким чином, у багатьох аспектах ітератором обробляється вказівник.

int arr [] = {0,1,2,3,4,5,6,7,8,9};

int * p = arr; // p вказує на перший елемент в обр

++ р; // p вказує на arr [1]

Так само, як ми можемо використовувати ітератори для переміщення елементів у векторі, ми можемо використовувати покажчики для переміщення елементів у масиві. Звичайно, для цього нам потрібно отримати вказівники на перший і один на останній елемент. Як ми щойно бачили, ми можемо отримати вказівник на перший елемент, використовуючи сам масив або взявши адресу першого елемента. Ми можемо отримати вказівник, що знаходиться в кінці, використовуючи інше спеціальне властивість масивів. Ми можемо взяти адресу неіснуючого елемента за останнім елементом масиву:

int * e = & arr [10]; // вказівник щойно минулого останнього елемента в arr

Тут ми використовували оператор індексів, щоб індексувати неіснуючий елемент; arr має десять елементів, тому останній елемент arr знаходиться в позиції індексу 9. Єдине, що ми можемо зробити з цим елементом, це взяти його адресу, яку ми робимо для ініціалізації e. Як і в кінцевому ітераторі (§ 3.4.1, стор. 106), вказівник без кінця не вказує на елемент. Як наслідок, ми не можемо знецінювати або збільшувати покажчик в кінці.

Це з видання C ++ праймер 5 видання Lipmann.

Отже, це UB не робити цього.


-4

У найсуворішому сенсі це не визначена поведінка, а визначена реалізацією. Тож, хоч і не доцільно, якщо ви плануєте підтримувати непрофільні архітектури, ви, ймовірно, можете це зробити.

Стандартна цитата, подана interjay, є хорошою, що вказує на UB, але це лише другий найкращий хіт, на мій погляд, оскільки він стосується арифметики покажчика-покажчика (смішно, один явно є UB, а інший - ні). У запитанні є абзац, що стосується операції безпосередньо:

[expr.post.incr] / [expr.pre.incr]
Операнд повинен бути [...] або вказівником на повністю визначений тип об'єкта.

О, почекайте мить, повністю визначений тип об’єкта? Це все? Я маю на увазі, справді, тип ? Тож вам об’єкт взагалі не потрібен?
Потрібно зовсім небагато читати, щоб насправді знайти натяк на те, що щось там може бути не таким добре визначеним. Тому що поки що він читає так, як ніби тобі дозволено це робити, жодних обмежень.

[basic.compound] 3робить заяву про те, який тип покажчика може бути, а як жоден з трьох інших, результат вашої операції явно підпадав би під 3.4: недійсний покажчик .
Однак це не говорить про те, що вам заборонено мати недійсний покажчик. Навпаки, у ньому перераховані деякі дуже поширені нормальні умови (наприклад, кінець тривалості зберігання), коли покажчики регулярно стають недійсними. Тож, мабуть, це може відбутися. І справді:

[basic.stc] 4
Непряме значення вказівника через недійсне значення вказівника та передача недійсного значення вказівника функції делокації мають невизначене поведінку. Будь-яке інше використання недійсного значення вказівника має поведінку, визначену реалізацією.

Ми робимо там "будь-який інший", тож це не визначена поведінка, а визначена реалізацією, таким чином загалом допустима (якщо тільки в реалізації явно не сказано щось інше).

На жаль, це ще не кінець історії. Хоча чистий результат з цього моменту більше не змінюється, він стає більш заплутаним, чим довше ви шукаєте "покажчик":

[basic.compound]
Дійсне значення типу вказівника об'єкта являє собою або адресу байта в пам'яті, або нульовий вказівник. Якщо об’єкт типу T розташований за адресою A [...], як кажуть, вказує на цей об’єкт, незалежно від того, як було отримано значення .
[Примітка. Наприклад, адреса, що минає наприкінці масиву, вважатиметься вказівкою на непов'язаний об'єкт типу елемента масиву, який може бути розташований за цією адресою. [...]].

Читайте як: Добре, хто дбає! Поки вказівник десь вказує на пам’ять , я добре?

[basic.stc.dynamic.safety] Значення вказівника - це безпечно отриманий покажчик [бла-бла]

Читайте як: Гаразд, безпечно отриманий, що завгодно. Це не пояснює, що це таке, а також не каже, що мені насправді це потрібно. Безпечно отриманий-чорт. Мабуть, я все ще можу мати непогано отримані вказівники просто чудово. Я здогадуюсь, що перенаправлення їх, мабуть, не буде такою вдалою ідеєю, але цілком допустимо їх мати. Це не говорить інакше.

Реалізація може мати послаблену безпеку вказівника, і в цьому випадку дійсність значення вказівника не залежить від того, чи це безпечно отримане значення вказівника.

О, так це може не мати значення, тільки те, що я думав. Але зачекайте ... "не може"? Це означає, що це також може бути . Звідки я знаю?

Альтернативно, реалізація може мати сувору безпеку вказівника; у цьому випадку значення вказівника, яке не є безпечно виведеним значенням вказівника, є недійсним значенням вказівника, якщо посилається повний об'єкт не має динамічної тривалості зберігання і раніше не було оголошено доступним

Зачекайте, тож навіть можливо, що мені потрібно дзвонити declare_reachable()на кожен покажчик? Звідки я знаю?

Тепер ви можете конвертувати в intptr_t, що є чітко визначеним, даючи ціле уявлення безпечно отриманого вказівника. Для яких, звичайно, будучи цілим числом, цілком законно і чітко визначено збільшувати його як завгодно.
І так, ви можете перетворити intptr_tспинку в покажчик, який також добре визначений. Тільки просто, не будучи початковим значенням, вже не гарантується, що у вас є безпечно отриманий покажчик (очевидно). Все-таки, що стосується літери стандарту, хоча це визначається впровадженням, це 100% законна справа:

[expr.reinterpret.cast] 5
Значення інтегрального типу або типу перерахування можна явно перетворити в покажчик. Вказівник, перетворений на ціле число достатнього розміру [...] і назад до того самого типу вказівника [...] початкового значення; відображення між покажчиками та цілими числами інакше визначено реалізацією.

Улов

Покажчики - це просто звичайні цілі числа, ви лише використовуєте їх як покажчики. О, якби тільки це було правдою!
На жаль, існують архітектури, де це зовсім не відповідає дійсності, а просто генерування недійсного вказівника (не дереферентування, просто наявність у реєстрі покажчиків) спричинить пастку.

Отже, це основа "реалізації визначено". Це, а також той факт , що збільшується покажчик щоразу , коли ви хочете, як ви , будь ласка , може , звичайно , перелив причини, які стандарт не хоче мати справу з. Кінець адресного простору програми може не збігатися з місцем розташування переповнення, і ви навіть не знаєте, чи є така річ, як переповнення покажчиків певної архітектури. Загалом це кошмарний безлад не в жодному відношенні можливих вигод.

Справа з умовою «один минулий об’єкт» з іншого боку, проста: реалізація повинна просто переконатися, що жоден об’єкт ніколи не виділяється, щоб останній байт в адресному просторі був зайнятий. Тож це чітко визначено, оскільки це корисно і банально гарантувати.


1
Ваша логіка хибна. "Тож вам об’єкт взагалі не потрібен?" неправильно трактує Стандарт, орієнтуючись на одне правило. Це правило стосується часу компіляції, незалежно від того, чи ваша програма добре сформована. Існує ще одне правило щодо часу виконання. Тільки під час запуску можна фактично говорити про існування об’єктів за певною адресою. ваша програма повинна відповідати всім правилам; правила часу компіляції під час компіляції та правила виконання часу на час виконання.
MSalters

5
У вас є схожі вади логіки з "ОК, хто дбає! Поки вказівник десь вказує на пам'ять, я добре?". Ні. Ви повинні дотримуватися всіх правил. Складна мова про те, що "кінець одного масиву починається з іншого масиву" просто дає дозвіл на реалізацію безперервного розподілу пам'яті; не потрібно зберігати вільний простір між розподілами. Це означає, що ваш код може мати те саме значення A, що і кінець одного об'єкта масиву та початок іншого.
MSalters

1
"Пастка" - це не те, що можна описати поведінкою, визначеною "реалізацією". Зауважте, що interjay знайшов обмеження для +оператора (з якого ++витікає), що означає, що вказівка ​​після "один за одним" не визначена.
Мартін Боннер підтримує Моніку

1
@PeterCordes: Прочитайте, будь-ласка , пункт 4 basic.stc . Він говорить "Непряме [...] не визначене поведінка. Будь-яке інше використання недійсного значення вказівника має поведінку, визначену реалізацією " . Я не плутаю людей, використовуючи цей термін для іншого значення. Це точне формулювання. Це не визначена поведінка.
Деймон

2
Навряд чи ви знайшли лазівку для посилення, але ви не цитуєте повний розділ про те, що відбувається після збільшення. Я зараз не збираюсь дивитись на це. Домовились, що якщо є, це ненавмисно. У будь-якому разі, як би приємно, якби ISO C ++ визначив більше речей для моделей з плоскою пам'яттю, @MaximEgorushkin, є й інші причини (наприклад, обертання покажчиків) для заборони довільних речей. Дивіться коментарі щодо Чи слід порівняння вказівників підписувати чи не підписувати у 64-бітному x86?
Пітер Кордес
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.