Чи визначено поведінку віднімання двох покажчиків NULL?


78

Чи визначено різницю двох змінних показників, що не є порожніми (для C99 та / або C ++ 98), якщо вони обидва NULLоцінюються?

Наприклад, скажімо, у мене є буферна структура, яка виглядає так:

struct buf {
  char *buf;
  char *pwrite;
  char *pread;
} ex;

Скажімо, ex.bufвказує на масив або якусь помилкову пам’ять. Якщо мій код завжди забезпечує це pwriteта preadвказує на цей масив або на один із нього, то я впевнений, що ex.pwrite - ex.preadце завжди буде визначено. Однак, що якщо pwriteі preadє NULL. Чи можу я просто розраховувати, що віднімання двох визначається як (ptrdiff_t)0або чи потрібно суворо сумісний код для перевірки покажчиків на NULL? Зауважте, що єдиний випадок, який мене цікавить, - це коли обидва вказівники мають значення NULL (що представляє буфер, не ініціалізований регістр). Причина пов’язана з повністю сумісною «доступною» функцією, враховуючи попередні припущення:

size_t buf_avail(const struct s_buf *b)
{     
    return b->pwrite - b->pread;
}

1
ви не раз пробували робити операцію?
Hunter McMillen

10
Що ви маєте на увазі? Я точно знаю, що результат цієї операції - 0 на 95% (припустимо, 5% - це AS / 400) реалізацій там, і нічого поганого не трапиться. Мене не цікавить специфіка впровадження. Моє запитання стосується деяких конкретних стандартних визначень.
Джон Любс,

8
Хантер Макміллен: Це поганий підхід - "Я зберігав покажчик в int, і нічого не сталося. Я перевіряю на іншому комп'ютері та компіляторі, і нічого не сталося. Потім з'явилися 64-розрядні комп'ютери". Якщо щось працює зараз, але покладається на невизначену поведінку, це може не спрацювати в майбутньому.
Мацей П'єхотка

3
Я вітаю вас за те, що ваш код гарантовано працює відповідно до відповідних стандартів, а не просто помічає, що він працював на тестованих платформах.
Девід Шварц

1
@TobySpeight: Компілятор 8086 матиме інше подання для near-кваліфікованого нульового вказівника від far-кваліфікованого, але чи використовував би він кілька подань для нульових farпокажчиків? Якщо нульовий nearпокажчик перетворюється на farпокажчик, який, у свою чергу, порівнюється з нульовим farпокажчиком, коли, наприклад, DS дорівнює 0x1234, що трапляється: (1) 0x0000 отримує значення 0x0000: 0x0000; (2) 0x0000 перетворюється на 0x1234: 0x0000, але оператор порівняння перевіряє випадки нуля обох сегментів, або (3) 0x0000 перетворюється на 0x1234: 0x0000, що порівнює нерівне з 0x0000: 0x0000.
supercat

Відповіді:


100

У C99 це технічно невизначена поведінка. C99 §6.5.6 говорить:

7) Для цілей цих операторів вказівник на об'єкт, який не є елементом масиву, поводиться так само, як покажчик на перший елемент масиву довжиною один із типом об'єкта як типом його елемента.

[...]

9) Коли віднімаються два вказівники, обидва вказують на елементи одного і того ж об'єкта масиву або одного минулого останнього елемента об'єкта масиву; результат - різниця індексів двох елементів масиву. [...]

І § 6.3.3.3 / 3 говорить:

Цілочисельний константний вираз зі значенням 0, або такий вираз, приведений до типу void *, називається константою нульового покажчика. 55) Якщо константа нульового вказівника перетворюється на тип вказівника, отриманий вказівник, який називається нульовим вказівником , гарантовано порівнює нерівне з покажчиком будь-який об'єкт або функцію.

Отож, оскільки нульовий покажчик нерівний для будь-якого об’єкта, він порушує передумови 6.5.6 / 9, тож це невизначена поведінка. Але на практиці я хотів би поспоритись, що майже кожен компілятор поверне результат 0 без жодних побічних ефектів.

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

З іншого боку, C ++ 03 має визначену поведінку в цьому випадку. Стандарт робить спеціальний виняток для віднімання двох нульових покажчиків. C ++ 03 §5.7 / 7 говорить:

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

C ++ 11 (як і останній проект C ++ 14, n3690) має ідентичне формулювання до C ++ 03, лише з незначною зміною std::ptrdiff_tзамість ptrdiff_t.


14
Завдяки повноті це найкраща відповідь на даний момент.
Джон Дайблінг

Це здається недоглядом стандарту, який слід виправити "9) Коли віднімаються два вказівники, якщо вони рівні, результат дорівнює нулю. В іншому випадку обидва вказуватимуть на елементи одного і того ж об'єкта масиву ..."
R .. GitHub STOP HELPING ICE

@R .., для усунення різниці слід сказати "порівняти рівне" ні? Оскільки два нульові покажчики можуть не містити однакового значення, тому вони "не" дорівнюють.
Jens Gustedt

Також схоже, що останній проект майбутнього стандарту C1X також має ту ж мову. Я сподіваюся, що це насправді недогляд і що мовний комітет це виправляє.
Адам Розенфілд,

Два нульові покажчики мають однакове значення внаслідок порівняння рівних. Звичайно, вони можуть не мати однакового представництва .
R .. GitHub СТОП ДОПОМОГАЙ ЛЕД

36

Я знайшов це у стандарті C ++ (5.7 [expr.add] / 7):

Якщо два вказівники [...] обидва є нульовими, а два вказівники віднімаються, результат порівнюється рівним значенню 0, перетвореному в тип std :: ptrdiff_t

Як сказали інші, C99 вимагає додавання / віднімання між 2 покажчиками на один і той же об'єкт масиву. NULL не вказує на дійсний об'єкт, тому ви не можете використовувати його для віднімання.


4
+1: Цікаво, тому C ++ чітко визначає цю поведінку, тоді як C - ні.
Олівер Чарлсворт

23

Редагувати : Ця відповідь дійсна лише для C, я не бачив тегу C ++, коли відповідав.

Ні, арифметика покажчика дозволена лише для вказівників, які вказують на один і той же об’єкт. Оскільки за визначенням стандартних нульових покажчиків C не вказують на будь-який об'єкт, це невизначена поведінка.

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


5
Це неправильно. Див. 5.7 [expr.add] / 7: "Якщо два вказівники вказують на один і той же об’єкт або обидва вказують один за одним кінцем одного масиву або обидва є нульовими , а два вказівники віднімаються, результат порівнюється рівним значенню 0 перетворено на тип std::ptrdiff_t".
CB Bailey

1
Це неправильно у конкретному контексті С ++. А як щодо інших мов із тегами?
Джон Дайблінг

8
Нічого собі, ніколи не думав, що це кульгаве питання торкнеться різниці специфікацій C / C ++.
Джон Любс,

3
@Jens: Поширені запитання та адміністратори сайтів закликають нас редагувати відповіді, якщо ми можемо зробити їх кращими. Ваша відповідь тепер краща, ніж була раніше. Я не хотів образити і, чесно кажучи, з огляду на політику сайту , я думаю , ви з лінії будуть ображені. Див: stackoverflow.com/privileges/edit
Джон Dibling

2
Йенс, як правило, я б погодився з тобою. Я намагаюся ніколи не редагувати чиюсь відповідь, а вказувати на свої незгоди коментарем - менше пір’я розтріпається, і вони можуть чомусь навчитися. А може, я натомість помиляюся, і моє редагування буде контрпродуктивним. Але в цьому випадку я думаю, що редагування Джона було виправданим, оскільки ваша відповідь була найвищою, але явно не на 100% правильна. Потрібно було утримати людей від "нагромадження" на правильну відповідь, не розглядаючи альтернативи.
Марк Ренсом

0

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

Для будь-якої відповідної реалізації С та майже всіх (якщо не всіх) реалізацій С-подібних діалектів, для будь-якого вказівника p, який *pабо *(p-1)ідентифікує якийсь об'єкт, діятимуть такі гарантії :

  • Для будь-якого цілочисельного значення, zяке дорівнює нулю, значення покажчика (p+z)і (p-z)буде еквівалентним у будь-якому випадку p, за винятком того, що вони будуть постійними лише в тому випадку, якщо обидва pі zє постійними.
  • Для будь- qякого, що еквівалентно p, вирази p-qі q-pдадуть нуль.

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

Якби існувало таке впровадження, де витрати на підтримку гарантій були б великими, але небагато, якщо будь-яка програма отримала б від них якусь вигоду, мало б сенс дозволити їй захоплювати обчислення "нуль + нуль" і вимагати, щоб код користувача для така реалізація включає ручну перевірку нуля, яку гарантії могли б зробити непотрібною. Очікувалось, що така надбавка не вплине на інші 99,44% реалізацій, де вартість підтримання гарантій перевищує вартість. Такі реалізації повинні підтримувати такі гарантії, але їх авторам не потрібно, щоб автори Стандарту повідомляли їм це.

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

Звичайно, сьогодні питання про те, що «визначено», рідко мають щось спільне з тим, яку поведінку може підтримувати платформа. Натомість зараз модно для компіляторів - в ім'я "оптимізації" - вимагати, щоб програмісти вручну писали код для обробки кутових випадків, з якими платформи раніше обробляли правильно. Наприклад, якщо код, який повинен виводити nсимволи, починаючи з адреси p, записується як:

void out_characters(unsigned char *p, int n)
{
  unsigned char *end = p+n;
  while(p < end)
    out_byte(*p++);
}

старіші компілятори генерували б код, який надійно нічого не видавав би, без побічних ефектів, якщо p == NULL та n == 0, без потреби в спеціальному випадку n == 0. Однак на новіших компіляторах потрібно було б додати додатковий код:

void out_characters(unsigned char *p, int n)
{
  if (n)
  {
    unsigned char *end = p+n;
    while(p < end)
      out_byte(*p++);
  }
}

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.