printf - джерело помилок? [зачинено]


9

Я використовую багато printfдля цілей відстеження / реєстрації в своєму коді, я виявив, що це джерело помилки програмування. Я завжди вважав, що оператор вставки ( <<) є дещо дивним, але я починаю вважати, що, використовуючи його, замість цього я міг уникнути деяких цих помилок.

Хто-небудь коли-небудь мав подібне одкровення чи я просто хапаюся за соломку тут?

Деякі забирають очки

  • Моя теперішня думка полягає в тому, що безпека типу переважає будь-яку перевагу використання printf. Справжня проблема - це форматний рядок та використання варіабельних функцій, не безпечних для типу.
  • Можливо, я не буду використовувати <<і варіанти виходу потоку stl, але я неодмінно вивчу використання безпечного для механізму типу, який дуже схожий.
  • Багато трасування / ведення журналів є умовними, але я хотів би завжди виконувати код, щоб не пропускати помилки в тестах, тільки тому, що це рідко взята гілка.

4
printfу світі С ++? Я щось тут пропускаю?
користувач827992

10
@ user827992: Ви пропускаєте той факт, що стандарт C ++ включає бібліотеку стандартних стандартів С за посиланням? Це абсолютно законно використовувати printfв C ++. (Чи хороша ідея - інше питання.)
Кіт Томпсон

2
@ user827992: printfмає деякі переваги; дивіться мою відповідь.
Кіт Томпсон

1
Це питання досить прикордонне. "Що ви думаєте", питання часто закриваються.
dbracey

1
@vitaut Я здогадуюсь (дякую за пораду). Мене просто трохи бентежить агресивна помірність. Це насправді не сприяє цікавим дискусіям щодо ситуацій програмування, про що я хотів би мати більше.
Джон Лейдегрен

Відповіді:


2

printf, особливо у випадках, коли ви можете піклуватися про продуктивність (наприклад, sprintf та fprintf) - це дійсно дивний злом. Мене постійно дивує, що люди, які стукають на C ++ через мізерну продуктивність, пов’язану з віртуальними функціями, потім продовжуватимуть захищати іо С.

Так, для того, щоб визначити формат нашого виводу, щось, що ми можемо знати на 100% під час компіляції, давайте розберемо стрункий формат під час виконання всередині масово дивної таблиці стрибків за допомогою непереборних кодів формату!

Звичайно, ці формати кодів не можна зробити так, щоб вони відповідали типам, які вони представляють, це було б занадто просто ... і вам нагадують щоразу, коли ви шукаєте, чи це% llg або% lg, що ця (сильно набрана) мова робить вас з'ясувати типи вручну, щоб щось надрукувати / сканувати, і розраховано на процесори до 32 біт.

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

(Якщо ви взагалі використовуєте стандартні контейнери, напишіть собі кілька швидких шаблонових операторів << перевантажень, які дозволяють робити такі дії, як std::cout << my_list << "\n";для налагодження, де тип my_list має тип list<vector<pair<int,string> > >.)


1
Проблема стандартної бібліотеки C ++ полягає в тому, що більшість втілень реалізуються operator<<(ostream&, T), викликаючи ... ну sprintf,! Продуктивність sprintfне є оптимальною, але завдяки цьому продуктивність іостримів, як правило, ще гірша.
Ян Худек

@JanHudec: На даний момент це не було правдою вже близько десяти років. Фактична друк виконується за допомогою тих самих базових системних викликів, і для C ++ реалізацій часто для цього звертаються в бібліотеки C ... але це не те саме, що маршрутизація std :: cout через printf.
jkerian

16

Змішування виходу в стилі С printf()(або puts()або putchar()або ...) з виходом у стилі C ++ std::cout << ...може бути небезпечним. Якщо я пам'ятаю правильно, вони можуть мати окремі механізми буферизації, тому вихід може не відображатися в запланованому порядку. (Як згадує AProgrammer у коментарі, sync_with_stdioце вирішує).

printf()є принципово небезпечним для типу. Тип, очікуваний для аргументу, визначається рядком формату ( "%d"вимагає intабо щось, що сприяє int, "%s"вимагає, char*який повинен вказувати на правильно закінчений рядок у стилі C тощо), але передача неправильного типу аргументу призводить до не визначеної поведінки , не діагностована помилка. Деякі компілятори, такі як gcc, досить добре працюють з попередженням про невідповідність типів, але вони можуть робити це лише у тому випадку, якщо рядок формату є буквальним чи іншим чином відомий під час компіляції (що є найпоширенішим випадком) - і таке мова не вимагає попереджень. Якщо ви передаєте неправильний тип аргументів, можуть трапитися довільно погані речі.

З іншого боку, потік вводу-виводу C ++ набагато більш безпечний <<для типів , оскільки оператор перевантажений для багатьох різних типів. std::cout << xне потрібно вказувати тип x; компілятор генерує правильний код для будь-якого типу x.

З іншого боку, printfпараметри форматування IMHO набагато зручніші. Якщо я хочу надрукувати значення з плаваючою комою з 3 цифрами після десяткової крапки, я можу використовувати "%.3f"- і це не впливає на інші аргументи, навіть у межах одного printfдзвінка. З setprecisionіншого боку, C ++ впливає на стан потоку і може зіпсувати пізніший результат, якщо ви не дуже обережні відновити потік до його попереднього стану. (Це мій особистий вихованець, якщо я пропускаю чистий спосіб уникнути цього, будь ласка, прокоментуйте.)

Обидва мають переваги та недоліки. Наявність printfособливо корисного, якщо у вас є C-фон і ви з ним більше знайомі, або якщо ви імпортуєте вихідний код C в програму C ++. std::cout << ...є більш ідіоматичним для C ++ і не потребує такої обережності, щоб уникнути невідповідностей типів. Обидва є дійсними C ++ (стандарт C ++ включає більшість стандартних бібліотек C за посиланням).

Це, мабуть, найкраще використовувати std::cout << ...заради інших програмістів на C ++, які можуть працювати над вашим кодом, але ви можете використовувати будь-який, особливо в трассинговому коді, який ви збираєтеся викинути.

І звичайно, варто витратити трохи часу на те, як використовувати налагоджувачі (але це може бути неможливо в деяких умовах).


В оригінальному питанні не згадується змішування.
dbracey

1
@dbracey: Ні, але я вважав, що це варто згадати як можливий недолік printf.
Кіт Томпсон

6
Про питання синхронізації див std::ios_base::sync_with_stdio.
AProgrammer

1
+1 Використання std :: cout для друку інформації про налагодження у багатопотоковому додатку на 100% марно. Принаймні, з printf речі не так імовірно будуть переплетені і нерозбірними людиною або машиною.
Джеймс

@James: Це тому, що std::coutвикористовується окремий дзвінок для кожного надрукованого елемента? Ви могли б обійти це, зібравши рядок виводу в рядок перед друком. І звичайно, ви також можете друкувати по одному предмету за раз printf; просто зручніше надрукувати рядок (або більше) за один дзвінок.
Кіт Томпсон

2

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

printf("%d", SomeObject);

Тоді <<як не буде.

Примітка: Для налагодження, ви не використовуєте printfабо cout. Ви використовуєте fprintf(stderr, ...)і cerr.


В оригінальному питанні не згадується змішування.
dbracey

Звичайно, ви можете надрукувати адресу об'єкта, але велика різниця полягає в тому, що printfце не безпечно для типу, і я думаю, що безпека типу переважає будь-яку користь від використання printf. Проблема полягає в дійсності рядка формату та не типової варіативної функції.
Джон Лейдегрен

@JohnLeidegren: Але що робити, якщо SomeObjectце не вказівник? ВАШ збирається отримати довільні двійкові дані, які компілятор вирішує SomeObject.
Linuxios

Я думаю, що я прочитав вашу відповідь назад ... nvm.
Джон Лейдегрен

1

Є багато груп - наприклад, Google - які не люблять потоків.

http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Streams

(Поп відкрийте трикутник, щоб ви могли бачити дискусію.) Я думаю, що в посібнику зі стилю google C ++ є багато дуже розумних порад.

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


2
Посібник зі стилів Google є приємним, але він містить досить багато предметів, які не підходять для посібника загального призначення . (що нормально, адже зрештою, це посібник Google щодо коду, який працює в / для Google.)
Мартин Ба

1

printfможе спричинити помилки через відсутність безпеки типу. Є кілька способів адресації , які не перемикаючись на iostream«s <<оператор і більш складне форматування:

  • Деякі компілятори (наприклад, GCC та Clang) можуть необов'язково перевіряти printfрядки формату на printfаргументи та можуть відображати попередження, такі як наступні, якщо вони не збігаються.
    попередження: конверсія вказує тип 'int', але аргумент має тип 'char *'
  • Typesafeprintf сценарій може препроцессіровать ваші printf-Style викликів , щоб зробити їх тіпобезопаснимі.
  • Бібліотеки, такі як Boost.Format і FastFormat, дозволяють використовувати printfрядки у формі формату (зокрема Boost.Format майже ідентичні printf), зберігаючи iostreamsбезпеку типу та розширення типу.

1

Синтаксис Printf в основному відмінний, за вирахуванням деяких неясних типів тексту. Якщо ви думаєте, що це неправильно, чому C #, Python та інші мови використовують дуже подібну конструкцію? Проблема в C або C ++: вона не є частиною мови і, таким чином, не перевіряється компілятором на правильний синтаксис (*) і не розкладається на ряд нативних викликів при оптимізації для швидкості. Зауважте, що якщо оптимізувати розмір, дзвінки printf можуть виявитися ефективнішими! Синтаксис потокового C ++ - це не що інше, як добре. Це працює, безпека типу є, але багатослівний синтаксис ... bleh. Я маю на увазі, я його використовую, але без радості.

(*) деякі компілятори роблять цю перевірку плюс майже всі інструменти статичного аналізу (я використовую Lint і з тих пір ніколи не виникало проблем з printf).


1
Існує Boost.Format, який поєднує зручний синтаксис ( format("fmt") % arg1 % arg2 ...;) з типом безпеки. Ціною трохи більшої продуктивності, тому що він генерує потокові виклики, які внутрішньо генерують виклики sprintf у багатьох реалізаціях.
Ян Худек

0

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

printf ( "%d in ANSI = %c\n", j, j ); /* Perfectly valid... if a char ISN'T printing right, I'd just check the integer value to make sure it was okay. */

Однак там, де ви можете використовувати <<оператор CPP, це коли ви перевантажуєте його для певного методу ... наприклад, щоб отримати дамп об'єкта, який зберігає дані конкретної людини, PersonData....

ostream &operator<<(ostream &stream, PersonData obj)
{
 stream << "\nName: " << name << endl;
 stream << " Number: " << phoneNumber << endl;
 stream << " Age: " << age << endl;
 return stream;
}

Для цього було б набагато ефективніше сказати (якщо припустити a, що це об'єкт PersonData)

std::cout << a;

ніж:

printf ( "Name: %s\n Number: %s\n Age: %d\n", a.name, a.number, a.age );

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


0

Ви не повинні використовувати printfв C ++. Колись. Причина, як ви правильно зазначали, полягає в тому, що це джерело помилок і те, що друкувати спеціальні типи, а в C ++ майже все повинно бути на замовлення, - це біль. Рішення C ++ - це потоки.

Однак існує критична проблема, яка робить потоки непридатними для будь-якого та видимого користувачем виводу! Проблема - це переклади. Приклад позики з посібника gettext говорить про те, що ви хочете написати:

cout << "String '" << str << "' has " << str.size() << " characters\n";

Тепер німецький перекладач підходить і каже: Гаразд, німецькою мовою, повідомлення повинно бути

n Zeichen lang ist die Zeichenkette ' s '

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

printf("%2$d Zeichen lang ist die Zeichenkette '%1$s'", ...);

У Boost.Format підтримує формати PRINTF стилю і має цю функцію. Отже, ви пишете:

cout << format("String '%1' has %2 characters\n") % str % str.size();

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


-1

Ви робите багато марної роботи, окрім того, що stlце зло чи ні, налагоджуйте свій код рядом printfлише додайте ще 1 рівень можливих збоїв.

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

PS

printf використовується в C, для C ++ у вас є std::cout


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