Чому printf (“% f”, 0); дати невизначену поведінку?


87

Заява

printf("%f\n",0.0f);

відбитки 0.

Однак заява

printf("%f\n",0);

друкує випадкові значення.

Я усвідомлюю, що демонструю якусь невизначену поведінку, але не можу зрозуміти, чому саме.

Значення з плаваючою комою, в якому всі біти дорівнюють 0, все ще є дійсним floatзі значенням 0.
floatІ intмають однаковий розмір на моїй машині (якщо це навіть актуально).

Чому використання цілочислового літералу замість літералу з плаваючою комою printfспричиняє таку поведінку?

PS таку ж поведінку можна побачити, якщо я використовую

int i = 0;
printf("%f\n", i);

37
printfочікує double, а ви йому даєте int. floatі intможе мати однаковий розмір на вашій машині, але 0.0fнасправді перетворюється на а doubleпри натисканні у варіантний список аргументів (і printfочікує цього). Коротше кажучи, ви не виконуєте свій кінець угоди printfна основі специфікаторів, які ви використовуєте, і аргументів, які ви надаєте.
WhozCraig

22
Varargs-функції не автоматично перетворюють аргументи функції у тип відповідного параметра, оскільки вони не можуть. Необхідна інформація недоступна для компілятора, на відміну від функцій не-varargs з прототипом.
EOF,

3
Ооо ... "варіадики". Я щойно вивчив нове слово ...
Mike Robinson


3
Наступне, що потрібно спробувати - це передати (uint64_t)0замість 0і перевірити, чи все-таки ви отримуєте випадкову поведінку (припускаючи doubleі uint64_tоднакові розміри та вирівнювання). Швидше за все, результат буде все одно випадковим на деяких платформах (наприклад, x86_64), оскільки різні типи передаються в різні регістри.
Ian Abbott

Відповіді:


121

"%f"Формат вимагає аргумент типу double. Ви даєте йому аргумент типу int. Ось чому поведінка невизначена.

Стандарт не гарантує, що all-bit-zero є дійсним поданням 0.0(хоча це часто і є), або будь-якого doubleзначення, або цього intі doubleмають однаковий розмір (пам'ятайте, це doubleні float), або, навіть якщо вони однакові size, що вони передаються як аргументи варіадичній функції таким же чином.

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

N1570 7.21.6.1 параграф 9:

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

Аргументи типу floatпросуваються double, саме тому це printf("%f\n",0.0f)працює. Аргументи цілочисельних типів вужчі, ніж intпросуваються до intабо до unsigned int. Ці правила просування (визначені пунктом 6 N1570 6.5.2.2) не допомагають у випадку printf("%f\n", 0).

Зверніть увагу, що якщо ви передаєте константу 0неріадичній функції, яка очікує doubleаргумент, поведінка є чітко визначеною, припускаючи, що прототип функції видно. Наприклад, sqrt(0)(after #include <math.h>) неявно перетворює аргумент 0з intна double- тому що компілятор бачить з оголошення, sqrtщо він очікує doubleаргументу. Він не має такої інформації для printf. Варіадичні функції, такі printfяк особливі, вимагають більше уваги при написанні дзвінків до них.


13
Тут є кілька чудових основних моментів. По-перше, це doubleне floatтак, припущення про ширину OP може не мати (можливо, ні). По-друге, припущення, що ціле число нуль та нуль з плаваючою комою мають однаковий бітовий шаблон, також не виконується. Хороша робота
Гонки легкості на орбіті

2
@LucasTrzesniewski: Гаразд, але я не бачу, як моя відповідь викликає питання. Я заявив, що floatпідвищують, doubleне пояснюючи чому, але це не було головним моментом.
Кіт Томпсон,

2
@ robertbristow-johnson: Компіляторам не потрібно мати спеціальних хуків printf, хоча gcc, наприклад, їх має, тому він може діагностувати помилки ( якщо рядок формату є буквальним). Компілятор може бачити декларацію printffrom <stdio.h>, яка повідомляє, що першим параметром є a, const char*а решта позначені , .... Ні, %fє для doublefloatпідвищується до double), і %lfє для long double. Стандарт C нічого не говорить про стек. Він визначає поведінку printfлише тоді, коли його правильно викликано.
Кіт Томпсон,

2
@ robertbristow-johnson: У старовинному запамороченні "lint" часто виконував деякі додаткові перевірки, які зараз виконує gcc. floatПередається printfпідвищується до double; в цьому немає нічого магічного, це просто мовне правило для виклику варіадичних функцій. printfсам знає через рядок форматування, що абонент стверджував, що йому передає; якщо це твердження неправильне, поведінка невизначена.
Кіт Томпсон,

2
Мале Виправлення: lдовжина модифікатор «не має ніякого ефекту на наступному a, A, e, E, f, F, g, або Gспецифікатор перетворення», довжина модифікатор для long doubleконверсії L. (@ robertbristow-johnson також може бути зацікавлений)
Даніель Фішер,

58

По-перше, як це торкалося кількох інших відповідей, але, на мій погляд, не було прописано досить чітко: це працює, щоб забезпечити ціле число у більшості контекстів, де функція бібліотеки приймає аргумент doubleабо floatаргумент. Компілятор автоматично вставить перетворення. Наприклад, sqrt(0)є чітко визначеним і буде поводитися точно так само sqrt((double)0), і те саме стосується будь-якого іншого цілочисельного виразу, що використовується там.

printfінакший. Це інакше, оскільки для цього потрібно змінювати кількість аргументів. Його функціональним прототипом є

extern int printf(const char *fmt, ...);

Тому, коли ви пишете

printf(message, 0);

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

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

Тепер інша половина запитання полягала в тому, що, враховуючи, що (int) 0 та (float) 0.0 у більшості сучасних систем обидва представлені як 32 біти, всі з яких дорівнюють нулю, чому це все одно не працює, випадково? Стандарт С просто говорить: "це не потрібно для роботи, ти сам на собі", але дозвольте мені пояснити дві найпоширеніші причини, чому це не спрацювало; це, мабуть, допоможе вам зрозуміти, чому це не потрібно.

По- перше, з історичних причин, коли ви проходите floatчерез змінний список аргументів він отримує підвищений до double, який, в більшості сучасних систем, становить 64 біт. Отже, printf("%f", 0)передає лише 32 нульові біти абоненту, який очікує 64 з них.

Друга, не менш важлива причина полягає в тому, що аргументи функції з плаваючою комою можуть передаватися в іншому місці, ніж цілі аргументи. Наприклад, більшість процесорів мають окремі регістрові файли для цілих чисел і значень з плаваючою комою, тому може бути правилом, що аргументи від 0 до 4 надходять у регістри від r0 до r4, якщо вони цілі числа, але від f0 до f4, якщо вони мають плаваючу крапку. Тож printf("%f", 0)шукає в реєстрі f1 цей нуль, але його взагалі немає.


1
Чи існують архітектури, які використовують регістри для варіативних функцій, навіть серед тих, що використовують їх для звичайних функцій? Я вважав, що це було причиною того, що варіадичні функції повинні бути належним чином оголошені, хоча інші функції [крім функцій з аргументами float / short / char] можуть бути оголошені ().
Random832

3
@ Random832 На сьогоднішній день, єдина відмінність між умовою виклику варіадичної та звичайної функції полягає в тому, що до варіадичної можуть надходити додаткові дані, наприклад, підрахунок справжньої кількості поданих аргументів. В іншому випадку все йде точно в тому самому місці, як це було б для нормальної функції. Див., Наприклад, розділ 3.2 x86-64.org/documentation/abi.pdf , де єдиним спеціальним методом лікування варіадиків є підказка AL. (Так, це означає, що реалізація va_argнабагато складніша, ніж була раніше.)
zwol

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

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

@zwol: Ні, я думав про ret nінструкцію 8086, де nбуло жорстко закодоване ціле число, яке, отже, не було застосовано до варіадичних функцій. Однак я не знаю, чи справді якийсь компілятор C цим скористався (звичайно, не компілятори).
celtschk

13

Зазвичай, коли ви викликаєте функцію, яка очікує a double, але ви надаєте an int, компілятор автоматично перетворює на a doubleдля вас. З цим не трапляється printf, оскільки типи аргументів не вказані в прототипі функції - компілятор не знає, що слід застосовувати перетворення.


4
Крім того, printf() зокрема , розроблено таким чином, щоб його аргументи могли бути будь-якого типу. Ви повинні знати, якого типу очікується кожен елемент у рядку format, і ви повинні вказати його правильно.
Mike Robinson

@MikeRobinson: Ну, будь-який примітивний тип C. Що є дуже-дуже маленькою підмножиною з усіх можливих типів.
MSalters

13

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

Оскільки printf()не має набраних параметрів, крім const char* formatstringпершого. Для всього іншого використовується еліпсис у стилі с ( ...).

Просто вирішується, як інтерпретувати передані там значення відповідно до типів форматування, заданих у рядку форматування.

Ви мали б таку ж невизначену поведінку, як при спробі

 int i = 0;
 const double* pf = (const double*)(&i);
 printf("%f\n",*pf); // dereferencing the pointer is UB

3
Деякі конкретні реалізації printfможуть працювати так (за винятком того, що передані елементи є значеннями, а не адресами). Стандарт C не визначає, як працюють printf інші варіативні функції, він просто визначає їх поведінку. Зокрема, немає згадок про рамки стека.
Кіт Томпсон,

Маленька примха: printfмає один набраний параметр, рядок формату, який має тип const char*. До речі, питання позначене як C, так і C ++, і C справді є більш актуальним; Я б, мабуть, не взяв reinterpret_castза приклад.
Кіт Томпсон,

Просто цікаве спостереження: однакова невизначена поведінка, і, швидше за все, через ідентичний механізм, але з невеликою різницею в деталях: передаючи int, як у питанні, UB трапляється в printf при спробі інтерпретувати int як double - у вашому прикладі , це трапляється вже надворі, коли перенаправлення посилань pf ...
Аконкагуа,

@Aconcagua Додано роз'яснення.
πάντα ῥεῖ

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

12

Використання невідповідного printf()специфікатора "%f"та типу (int) 0призводить до невизначеної поведінки.

Якщо специфікація перетворення недійсна, поведінка невизначена. C11dr §7.21.6.1 9

Кандидатські причини УБ.

  1. Це UB на специфікацію, а компіляція є орнаментом - сказав Нуф.

  2. doubleі intмають різні розміри.

  3. doubleі intможуть передавати свої значення, використовуючи різні стеки (загальний проти стеку FPU .)

  4. double 0.0 Може же не бути визначена з допомогою всіх нуль бітів. (рідко)


10

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

$ gcc -Wall -Wextra -pedantic fnord.c 
fnord.c: In function ‘main’:
fnord.c:8:2: warning: format ‘%f’ expects argument of type ‘double’, but argument 2 has type ‘int’ [-Wformat=]
  printf("%f\n",0);
  ^

або

$ clang -Weverything -pedantic fnord.c 
fnord.c:8:16: warning: format specifies type 'double' but the argument has type 'int' [-Wformat]
        printf("%f\n",0);
                ~~    ^
                %d
1 warning generated.

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


9

Я не впевнений, що бентежить.

Ваш рядок формату очікує a double; ви надаєте замість цьогоint .

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


3
@Voo: Цей модифікатор рядка формату , на жаль, названий, але я все ще не розумію, чому ви думаєте, що тут intбуде прийнятним.
Гонки легкості на орбіті

1
@Voo: "(що також кваліфікується як дійсний шаблон з поплавком)" Чому intкваліфікується як дійсний шаблон з поплавком? Доповнення двох та різні кодування з плаваючою комою майже не мають нічого спільного.
Гонки легкості на орбіті

2
Це бентежить, оскільки для більшості функцій бібліотеки надання цілочисельного літералу 0для введеного аргументу doubleзробить правильну справу. Для початківців не очевидно, що компілятор не робить того самого перетворення для printfслотів аргументів, адресованих %[efg].
zwol

1
@Voo: Якщо вас цікавить, наскільки жахливо це може піти, врахуйте, що на x86-64 SysV ABI аргументи з плаваючою комою передаються в іншому наборі регістрів, ніж цілі числа.
EOF,

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

4

"%f\n"гарантує передбачуваний результат лише тоді, коли другий printf()параметр має тип double. Далі додаткові аргументи варіадичних функцій є предметом просування аргументів за замовчуванням. Цілочисельні аргументи підпадають під цілочисельне просування, що ніколи не призводить до введених значень із плаваючою комою. І floatпараметри підвищуються до double.

До того ж: стандарт дозволяє другому аргументу бути або floatабо doubleнічого іншого.


4

Чому це формально UB, зараз обговорювалось у декількох відповідях.

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

  • printfочікує своїх аргументів відповідно до стандартного розповсюдження vararg. Це означає, що a floatбуде a, doubleа все, що менше, ніж intбуде int.
  • Ви передаєте intде, де функція очікує a double. Ваш int, мабуть, 32 біт, ваш double64 біт. Це означає, що чотири байта стека, що починаються з місця, де повинен знаходитися аргумент, є 0, але наступні чотири байти мають довільний вміст. Це те, що використовується для побудови відображуваного значення.

0

Основна причина цієї проблеми "невизначеного значення" полягає у приведенні покажчика до intзначення, яке передається до розділу printfзмінних параметрів до покажчика на doubleтипи, які va_argвиконує макрос.

Це спричиняє посилання на область пам'яті, яка не була повністю ініціалізована зі значенням, переданим параметром printf, оскільки doubleобласть буфера пам'яті intрозміру перевищує розмір.

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


Він може розглянути ці конкретні частини реалізацій простого коду "printf" та "va_arg" ...

printf

va_list arg;
....
case('%f')
      va_arg ( arg, double ); //va_arg is a macro, and so you can pass it the "type" that will be used for casting the int pointer argument of printf..
.... 


реальна реалізація у vprintf (враховуючи імпульс gnu) подвійного значення параметрів управління кодом справи:

if (__ldbl_is_dbl)
{
   args_value[cnt].pa_double = va_arg (ap_save, double);
   ...
}



va_arg

char *p = (double *) &arg + sizeof arg;  //printf parameters area pointer

double i2 = *((double *)p); //casting to double because va_arg(arg, double)
   p += sizeof (double);



посилання

  1. проект gnu glibc реалізація "printf" (vprintf))
  2. приклад семпліфікаційного коду printf
  3. приклад коду семпліфікації va_arg
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.