Чому це твердження, що вимагається перенаправлення на тип-покарання покажчика, що відповідає компілятору попередження?


38

Я читав різні повідомлення про Stack Overflow RE: помилка вказівника, що відміняє відмітку. Я розумію, що помилка - це по суті попередження компілятора про небезпеку доступу до об'єкта через вказівник іншого типу (хоча, як видається, робиться виняток char*), що є зрозумілим і розумним попередженням.

Моє запитання стосується коду, наведеного нижче: чому передача адреси вказівника відповідає void**умовам цього попередження (сприяє помилці через -Werror)?

Більше того, цей код складено для декількох цільових архітектур, лише одна з яких генерує попередження / помилку - чи може це означати, що він законно є дефіцитом версії компілятора?

// main.c
#include <stdlib.h>

typedef struct Foo
{
  int i;
} Foo;

void freeFunc( void** obj )
{
  if ( obj && * obj )
  {
    free( *obj );
    *obj = NULL;
  }
}

int main( int argc, char* argv[] )
{
  Foo* f = calloc( 1, sizeof( Foo ) );
  freeFunc( (void**)(&f) );

  return 0;
}

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

Чи є спосіб вирішення питання, що не використовується lvalues, які б утихомирили це специфічне для компілятора попередження / помилку? Тобто я розумію, що і чому це вирішить проблему, але я хотів би уникнути такого підходу, тому що я хочу скористатись freeFunc() НУЛЬНІСТЬМИ передбачуваного виводу:

void* tmp = f;
freeFunc( &tmp );
f = NULL;

Компілятор проблем (один з одного):

user@8d63f499ed92:/build$ /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc --version && /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-fc3-linux-gnu-gcc (GCC) 3.4.5
Copyright (C) 2004 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

./main.c: In function `main':
./main.c:21: warning: dereferencing type-punned pointer will break strict-aliasing rules

user@8d63f499ed92:/build$

Компілятор, що не скаржиться (один із багатьох):

user@8d63f499ed92:/build$ /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc --version && /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-rh73-linux-gnu-gcc (GCC) 3.2.3
Copyright (C) 2002 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

user@8d63f499ed92:/build$

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



1
"a void**, будучи ще лише вказівником, це має бути безпечним литтям." Вау там скупий! Здається, у вас є деякі принципові припущення. Постарайтеся думати менше в плані байтів і важелів, а більше - в абстракціях, адже саме це ви насправді програмуєте
Гонки

7
По суті, компілятори, якими ви користуєтеся, мають 15 та 17 років! Я б не покладався ні на одну.
Тавіан Барнс

4
@TavianBarnes Крім того, якщо вам потрібно покластися на GCC 3 з будь-якої причини, найкраще скористатись кінцевою версією, що закінчилася, яка була 3.4.6. Чому б не скористатися всіма доступними виправленнями до цієї серії, перш ніж вона була відкладена.
Каз

Який стандарт кодування C ++ прописує всі ці пробіли?
Пітер Мортенсен

Відповіді:


33

Значення типу void**- вказівник на об’єкт типу void*. Об'єкт типу Foo*не є об'єктом типу void*.

Існує неявна конверсія між значеннями типу Foo*та void*. Ця конверсія може змінити подання значення. Так само ви можете писати, int n = 3; double x = n;і це має чітко визначену поведінку встановлення xзначення 3.0, але double *p = (double*)&n;має невизначене поведінку (і на практиці не буде встановлено p"покажчик 3.0" на будь-яку загальну архітектуру).

Архітектури, де різні типи покажчиків на об’єкти мають різні уявлення, сьогодні є рідкісними, але це дозволено стандартом C. Існують (рідкісні) старі машини зі вказівниками на слова, які є адресами слова в пам'яті та байтовими вказівниками, які є адресами слова разом із зміщенням байтів у цьому слові; Foo*було б вказівником на слова і void*було б байтовим покажчиком на таких архітектурах. Існують (рідкісні) машини з жировими покажчиками, які містять інформацію не тільки про адресу об'єкта, а й про його тип, його розмір та списки контролю доступу; вказівник на певний тип може мати інше представлення від того, void*якому потрібна додаткова інформація про тип під час виконання.

Такі машини рідкісні, але дозволені стандартом С. А деякі компілятори C користуються дозволом на трактування покажчиків, що набиваються на типи, як окремі для оптимізації коду. Ризик зшивання покажчиків є головним обмеженням можливості компілятора оптимізувати код, тому компілятори, як правило, користуються такими дозволами.

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

Ви можете зробити freefuncмакрос:

#define FREE_SINGLE_REFERENCE(p) (free(p), (p) = NULL)

Це пов'язано зі звичайними обмеженнями макросів: відсутність безпеки типу, pоцінюється двічі. Зауважте, що це лише дає вам безпеку не залишати звисаючі вказівники навколо, якщо pбув єдиний вказівник на звільнений об’єкт.


1
І добре знати, що навіть якщо Foo*і void*мати однакове представлення на вашій архітектурі, все одно не визначено їх набирати.
Тавіан Барнс

12

Частина A void *розглядається спеціально стандартом C, оскільки вона посилається на неповний тип. Ця процедура ніяк НЕ поширюється на , void **як це робить точку повного типу, в зокрема void *.

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

Ви можете подолати це обмеження, використовуючи функціональний макрос замість функції:

#define freeFunc(obj) (free(obj), (obj) = NULL)

Якого ви можете назвати так:

freeFunc(f);

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

#define freeFunc(obj) ({ typeof (&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; })

3
+1 для кращої реалізації наміченої поведінки. Єдина проблема, яку я бачу в #defineтому, що вона оцінить objдвічі. Я не знаю хорошого способу уникнути цієї другої оцінки. Навіть вираз заяви (розширення GNU) не виконає хитрість, як вам потрібно призначити objпісля використання його значення.
cmaster

2
@cmaster: Якщо ви готові використовувати розширення GNU , такі як вираження заяви, то ви можете використовувати , typeofщоб уникнути оцінок objдвічі #define freeFunc(obj) ({ typeof(&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; }).
ruakh

@ruakh Дуже круто :-) Було б чудово, якби dbush відредагував це у відповідь, тому він не отримає масового видалення з коментарями.
cmaster - відновити

9

Перенаправлення покажчика типу покарання - це UB, і ви не можете розраховувати, що станеться.

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

Випадок, який може допомогти вам зрозуміти, чому тип покарання в цьому випадку може бути поганим, це те, що ваша функція не буде працювати в архітектурі, для якої sizeof(Foo*) != sizeof(void*). Це дозволено стандартом, хоча я не знаю жодного поточного, для якого це правда.

Приблизним рішенням буде використання макросу замість функції.

Зверніть увагу, що freeприймає нульові покажчики.


2
Захоплююче, що можливо так sizeof Foo* != sizeof void*. Я ніколи не стикався з типовими розмірами вказівників, що залежать від типу, тому впродовж багатьох років я вважаю аксіоматичним, що розміри вказівників однакові для даної архітектури.
StoneThrow

1
@Stonethrow стандартним прикладом є жирові покажчики, які використовуються для адреси байтів у архітектурі, адресованій словом. Але я думаю, що в сучасних машинах, що адресуються, використовується альтернативне слово sizeof char == sizeof .
AProgrammer

2
Зауважте, що тип має бути
Антті

@StoneThrow: Незалежно від розмірів вказівника, аналіз псевдонімів на основі типу робить його небезпечним; це допомагає компіляторам оптимізувати, припускаючи, що магазин через float*не змінить int32_tоб'єкт, тому, наприклад, компілятору int32_t*не потрібно int32_t *restrict ptrвважати, що він не вказує на ту саму пам'ять. Те саме для магазинів через void**істоту, яка передбачає, що не змінює Foo*об'єкт.
Пітер Кордес

4

Цей код недійсний відповідно до стандарту C, тому може працювати в деяких випадках, але не обов'язково є портативним.

"Правило суворого псевдоніму" для доступу до значення за допомогою покажчика, який передано іншому типу вказівника, міститься в пункті 6.5 пункту 6.5:

Об'єкт має збережене значення, доступ до якого має лише вираз lvalue, який має один із наступних типів:

  • тип, сумісний з ефективним типом об'єкта,

  • кваліфікована версія типу, сумісна з ефективним типом об'єкта,

  • тип, який є типовим або непідписаним типом, що відповідає ефективному типу об'єкта,

  • тип, який є типовим або непідписаним типом, що відповідає кваліфікованій версії ефективного типу об'єкта,

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

  • тип символів

У вашому *obj = NULL;операторі об'єкт має ефективний тип, Foo*але до нього звертається вираз lvalue *objз типом void*.

У пункті 6.7.5.1 пункту 2 ми маємо

Щоб два типи вказівників були сумісні, обидва мають бути однаково кваліфіковані, і обидва повинні бути вказівниками на сумісні типи.

Так void*і Foo*не сумісні типів або сумісні типи з відбірковими додано, і , звичайно , не відповідають жодному з інших варіантів суворого правила накладення спектрів.

Хоча код не є технічною причиною недійсним, він також має відношення до пункту 26 розділу 6.2.5:

Вказівник на voidповинен мати ті ж вимоги щодо представлення та вирівнювання, що і вказівник на тип символів. Аналогічно, покажчики на кваліфіковані або некваліфіковані версії сумісних типів повинні мати однакові вимоги щодо представлення та вирівнювання. Усі вказівники на типи структур мають однакові вимоги до представлення та вирівнювання, як один до одного. Усі вказівники на типи об'єднання мають однакові вимоги щодо представлення та вирівнювання, як один до одного. Покажчики на інші типи не повинні мати однакових вимог щодо представлення чи вирівнювання.

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


2

Крім того, що сказали інші відповіді, це класичний антидіапазон у С і той, який слід спалити вогнем. Він з'являється у:

  1. Функції безкоштовного і недійсного на зразок тієї, в якій ви знайшли попередження.
  2. Функції розподілу, які уникають стандартної C ідіоми повернення void *(яка не страждає від цієї проблеми, оскільки вона передбачає перетворення значення замість того, щоб карати тип ), замість цього повертає прапор помилки та зберігає результат за допомогою покажчика на покажчик.

Для іншого прикладу (1) існував давній сумнозвісний випадок у функції ffmpeg / libavcodec av_free. Я вважаю, що це врешті було виправлено макросом чи іншим трюком, але я не впевнений.

Для (2) і те, cudaMallocі інше posix_memalign- приклади.

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


Чи є у вас посилання, що пояснює більше про те, чому (1) є антидіаграмою? Я не думаю, що я знайомий з цією ситуацією / аргументом і хотів би дізнатися більше.
StoneThrow

1
@StoneThrow: Це дійсно просто - метою є запобігання неправомірному використанню, скасовуючи об'єкт, що зберігає вказівник, до звільненої пам'яті, але єдиний спосіб це реально зробити, це якщо абонент фактично зберігає вказівник в об'єкті введіть void *і перетворіть / перетворіть його щоразу, коли він захоче скинути його з ладу. Це дуже малоймовірно. Якщо абонент зберігає якийсь інший тип вказівника, єдиний спосіб викликати функцію без виклику UB - скопіювати покажчик на тимчасовий об’єкт типу void *та передати адресу цього на функцію звільнення, а потім просто ...
R .. GitHub СТОП ДОПОМОГАЄТЬСЯ

1
... нульовує тимчасовий об'єкт, а не власне сховище, де викликав те, що викликав. Звичайно, що насправді відбувається, це те, що користувачі функції в кінцевому підсумку роблять (void **)акторський склад , створюючи невизначене поведінку.
R .. GitHub СТОП ДОПОМОГАЙТЕ

2

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

Перед тим, як стандарт був написаний, реалізація для платформ, які використовували однакове представлення для всіх типів вказівників, одностайно дозволять використовувати a void**, принаймні, з відповідним кастингом, як "вказівник на будь-який покажчик". Автори стандарту майже напевно визнали, що це буде корисно на платформах, які підтримують його, але оскільки він не міг бути повсюдно підтриманий, вони відмовилися від мандату. Натомість вони розраховували, що впровадження якості обробить такі конструкції, як те, що в Обґрунтуваннях буде описано як "популярне розширення", у випадках, коли це буде мати сенс.

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