Я написал маленькую электронную книгу в которой рассматриваю вопросы как сделать код лучше. Книга ориентирована на Си/Си++ программистов, но будет интересна и разработчикам, использующих другие языки. Формат книги не подходит для моего любимого Хабра, но мне интересно получить обратную связь и обсудить мысли, изложенные в статье. Поэтому я решил разместить здесь только анонс, а с самой статьей можно познакомиться здесь. И приглашаю в комментарии для обсуждения.
Надеюсь вам понравился этот сборник советов. Конечно, предупредить о всех способах написать программу неправильно невозможно, да в этом и нет смысла. Моей целью было предостеречь программиста и развить в нем чувство опасности. Возможно, когда программист в очередной раз столкнется с чем-то непонятным, он вспомнит о моих наставлениях и не станет торопиться. Иногда несколько минут изучения документации или написание более простого/ясного кода позволит избежать внесения скрытой ошибки, которая затем несколько лет отравляла бы жизнь пользователям и коллегам.
Вот 42 темы, которые я рассматриваю:
- Не берите на себя работу компилятора
- Больше 0, это не 1
- Один раз скопируй, несколько раз проверь
- Бойтесь оператора ?: и заключайте его в круглые скобки
- Используйте доступные инструменты для проверки кода
- Проверьте все места, где указатель явно приводится к целочисленным типам
- Не вызывайте функцию alloca() внутри циклов
- Помните, что исключение в деструкторе — это опасно
- Используйте для обозначения терминального нуля литерал '\0'
- Старайтесь «не мельчить» при использовании #ifdef
- Не жадничайте на строчках кода
- Занимаясь Copy-Paste, сосредоточьтесь в конце работы
- Выравнивайте однотипный код «таблицей»
- Помните: не всегда достаточно компилятора и хорошего стиля кодирования
- Если есть возможность, начинайте использовать enum class.
- «Смотрите как я могу» — недопустимо в программировании
- Используйте специализированные функции для затирания в памяти приватных данных
- Знания, полученные при работе с одним языком, не всегда применимы к другому языку
- Как правильно вызвать один конструктор из другого
- Проверки о достижении конца файла (EOF) может быть недостаточно
- Проверяйте признак достижения конца файла (EOF) правильно
- Не используйте #pragma warning(default: X)
- Вычисляйте длину строкового литерала автоматически
- Идентификаторы override и final должны стать вашими новыми друзьями
- Больше не сравнивайте 'this' с nullptr
- Коварный VARIANT_BOOL
- Коварные BSTR строки
- Не делайте макрос там, где можно сделать обыкновенную функцию
- Используйте для итераторов префиксный оператор инкремента (++i) вместо постфиксного (i++)
- Visual C++ и функция wprintf()
- В C и C++ массивы не передаются по значению
- Бойтесь printf
- Никогда не разыменовывайте нулевые указатели
- Undefined behavior ближе, чем вы думаете
- Добавляя в enum новую константу, не забываем поправить операторы switch
- Если на вашем компьютере происходят магические события, проверьте память
- Бойтесь оператора continue внутри do {… } while(...)
- С сегодняшнего дня используйте nullptr вместо NULL
- Почему некорректный код иногда работает
- Внедрите статический анализ кода
- Сопротивляйтесь добавлению в проект новых библиотек
- Не давайте функциям название «empty»
Итак, ссылка на статью: http://www.viva64.com/ru/b/0391/
На английском языке: http://www.viva64.com/en/b/0391/
Я предвижу, что может нечаянно приключиться Хаброэффект, поэтому хочу заранее подстраховаться. Поэтому выкладываю на Яндекс Диск в формате PDF:
Русский: https://yadi.sk/i/LKkWupFjr5WzR
Английский: https://yadi.sk/i/pBZqebxsr5Wyg
P.S. Я специально не размещаю текст книги на других сайтах. Достаточно много людей сообщает о недочётах в тексте, и, если текст будет опубликован в нескольких местах, я замучаюсь вносить исправления. Когда текст стабилизируется, я размещу его ещё где-то в разных форматах. Пока же прошу отнестись с пониманием. И ещё просьба. О замеченных ляпах прошу писать мне на почту (karpov [@] viva64.com), а не в комментариях.
Комментарии (95)
daiver19
15.04.2016 15:50+4«Добавляя в enum новую константу, не забываем поправить операторы switch» — просто не надо добавлять default в switch для enum (ваши же советы о том, что не надо выполнять работу за компилятор и экономить на строках.
m1n7
15.04.2016 16:17-2Пункт 13, самый конец.
, «KW_IF»
, «KW_IGNOREPAT»
, «KW_INCLUDES»
, «KW_JUMP»
, «KW_MACRO»
, «KW_PATTERN»
Очевидно, в первой строке запятая не нужна. На этом фоне довольно иронично смотрится «Теперь очень легко заметить недостающую запятую»gorodnev
17.04.2016 19:47Запятая в первой строке не очень страшна, т.к. вызовет ошибку компиляции. Да, неприятно, да немного не чисто, но компилятор все равно найдет эту ошибку и подскажет, что нужно подправить.
zikher
15.04.2016 16:50Добавляя в enum новую константу, не забываем поправить операторы switch
Если enum представляет собой последовательные номера (или не сильно разрежённые) и начинается с 0, а количество элементов достаточно большое, то проще сделать таблицу функций. Ошибка будет легко находиться на этапе компиляции, если последним элементом enum-а сделать MYENUM_MAX и сделать эту константу размером таблицы функций.
И выглядит такой код зачастую привлекательнее, чем гигантский switch.SKolotienko
15.04.2016 23:33Но если есть гигантский switch и хочется иметь compile-time проверку, то можно также добавить static_assert на равенство MYENUM_MAX == x, где x — то количество элементов в перечислении, которое было в момент написание switch-а.
vagran
16.04.2016 09:34+1Если нет default, хороший компилятор и так ругнётся на недостающие значения.
ZyXI
17.04.2016 02:14Какая ошибка компиляции?
typedef void (*MyEnumHandler)(void); MyEnumHandler example_handler; const MyEnumHandler table[10] = { &example_handler, };
является вполне правильным кодом. «Лишние» элементы будут просто нулями. Вот switch даст ошибку компиляции (при -Werror). А таблица нет, она даст ошибку только если из enum удалили значение и MYENUM_MAX стало меньше длины таблицы.
Плюс нужно либо синхронизировать порядок между таблицей и enum, что неудобно и не даёт группировать обработчики в таблице, что можно делать со switch. Либо нужно использовать
[kMyEnumFoo] = &example_handler
. Такой синтаксис также нужен, чтобы получить ошибку, если элемент перечисления был удалён, но зато вставили новый (switch тоже даёт так сделать). Но это C99. А в C89 и ниже switch имеет ещё больше преимуществ перед таблицей, а не одну только ошибку компиляции при добавлении нового элемента в перечисление.zikher
18.04.2016 09:28Да, насчёт ошибки компиляции ошибся.
Но по мне, таблицы функций удобнее хотя бы тем, что можно все обработчики данного enum сложить в одно место (таблицу структур с указателями на функции).
Со switch-ем есть ещё две проблемы: забытый break и зачастую расползающиеся простыни кода, который явно просится выделиться в функции, но его никто не рефакторит.
nikkadim
15.04.2016 17:05Можете объяснить п.29?
Andrey2008
15.04.2016 17:05Попробую. Что именно следует пояснить?
dyadyaSerezha
15.04.2016 18:06+1Не поверите, но п.29
khim
15.04.2016 20:22А чего тут пояснять? "++i" просто проще…
"++i" обозначает, что переменную можно увеличить — и сразу использовать. «i++» обозначает, что нужно где-то «прикопать» предыдущее значение. Что может быть дорого. В случае если компилятор «видит» все определения он, конечно, справится с оптимизацией и лишнюю работу изведёт, но в любом случае — зачем его заставлять делать это, если можно этого и не делать? Ещё и человек может запутаться во всём этом…
tsostik
15.04.2016 20:53Стандартным объяснением для такого совета является примерно следующий:
++i меняет итератор, после чего возвращает ссылку на обновленный итератор.
i++ сохраняет копию итератора, меняет итератор, возвращает сохраненный. Итого имеем на один конструктор копирования больше.
Впрочем, мне встречалось и противоположное утверждение о том, что постинкремент предпочтительнее, правда относилось оно к интегральным типам. Обоснования, увы, не помню, но выглядело оно не лишенным логики.vladon
16.04.2016 00:57-1потому что в цикле в случае с i++ сперва идет чтение из регистра, а затем его увеличение, и процессор умеет такое делать параллельно
в случае с ++i сперва нужно увеличить, а затем считать, что распараллелить невозможно
это касается тех типов, которые влезают в регистрkhim
16.04.2016 02:25+7Это называется «слышал звон, да не знаю — где он».
Чтение/запись и вычисления на большинстве процессоров делаются совершенно отдельными блоками, которые, уж конечно, могут всё распараллелить. Или не могут — если процессор совсем старый.
Никакой разницы между преинкрементом и постинкрементом нет. Но это в сегодняшенем мире разницы нет, когда-то она действительно была — только к паралльным вычислениям чего-либо она отношения не имеет :-)
Разница была в PDP-11. Вот там — да, «i++» и «--i» выгоднее, чем «++i» и «i--». Но это ни с каким «параллельным исполнением» не связано — просто так система команд устроена, что в ней есть постинкремент и предекремент, а постдекремента и преинкремента нету.
Такая особенность, в свою очередь, тоже имеет объяснение: так как на PDP-11 все регистры — регистры общего назначения (влючая PC и SP) и доступ к ним всем осуществляется по одному шаблону, то нужен «перекос», чтобы нормально мог работать стек, а так как immediate'ов тоже нет, то нужно, чтобы у команды читающей по адресу PC происходил именно постинкремент, а не преинкремент. Так и получилось что в наборе способов обращения к памяти получился вот такой странный «перекос»: один бит экономии на опкоде, однако.
Оттуда всё и пошло. Но, чёрт побери, PDP-11 перестала выпускаться ещё до того, как большинство читателей Хабра на свет появились! Уже ARM в 1987м имел как LDMIB/STMIB (load/store memory increment before), так и LDMIA/STMIA (load/store memory increment after) — и в обоих случаях регистр увеличивается в том же такте, когда и используется!
Может уже стоит забыть про «дела давно минувших дней»?vladon
16.04.2016 07:35Вот нашел видео, откуда я это взял, и там речь явно не про pdp-11:
https://youtu.be/vrfYLlR8X8k
примерно 48:10khim
16.04.2016 12:22+1Вот что бывает, когда водители такси начинают смотреть передачи, где профессиональные гонщики обсуждают как лучше проходить поворот в заносе на льду :-)
Там речь шла об очень специфическом случае: 64-битный процессор, 32-битный индекс (который может получить экономию, если у вас их много), вы хотите их смешать (однократно) и обратиться по этому индексу в память.
Вот в этом, очень специфическом, случае действительно иногда, очень редко, вы можете получить выигрыш! Из-за использования 32-битного индекса у вас появляется дополнительная операция между увеличением i и его использованием: обрезание (если процессор не имеет 32-битных операций) или «расширение» (если имеет).
И вот тут действительно может получиться проблемка. А может и не получиться. Причём в реальной жизни — обычно не получается. Дело в том, что если индекс у вас «просто int», то он не может переполниться (неопределённое поведение) и, в большинстве случаев, за счёт этого компилятор сможет вернуть «утерянный параллелизм».
Вот uinsigned int — тут да, тут у компилятора определённо проблемы возникнут.
Никакого отношения к итераторам этот пример не имеет. Вообще. А про использование индексов и указателей — рекомендую послушать ровно предыдущую минуту того же выступления.
hellamps
16.04.2016 14:44В видео ситуация немножко другая, там идет обращение к элементу массива по инкрементируюмуся указателю, и если постинкремент позволяет сначала использовать смещение для доступа к памяти, а потом увеличивать указатель(пока dma работает), то сначала инкремент заставляет сначала ждать инкремент, а потом запускать фетч из памяти.
По идее, еще быстрее должно работать что-то типа *a++ =…
Но в реальной жизни, думаю, компилятор из всех вариантов сделает одно и то же.khim
16.04.2016 16:12В видео ситуация немножко другая, там идет обращение к элементу массива по инкрементируюмуся указателю, и если постинкремент позволяет сначала использовать смещение для доступа к памяти, а потом увеличивать указатель(пока dma работает), то сначала инкремент заставляет сначала ждать инкремент, а потом запускать фетч из памяти.
Какой ещё DMA? Процессоры DMA не используют. Зато они активно используют prefetch, переименование регистров и спекулятивное исполнение…
Но в реальной жизни, думаю, компилятор из всех вариантов сделает одно и то же.
Если сможет. Как я уже написал выше — у него может не быть такой возможности. Я пробовал сделать так, чтобы «не получилось» — но примеры получаются весьма хитро закрученными, а выигрыш — мизерным. То есть легко сделать так, чтобы компилятору пришлось породить лишний «mov», но это мало: в современных процессорах mov «ничего не стоит», то есть нужно ещё сделать так, чтобы эта лишняя команда «напрягла декодер» и так далее. Это очень сложно сделать. То есть если вы хотите выиграть «самый последний такт», то… может тогда уже стоит на ассемблер посмотреть и отойти от C/C++?
Я думаю та строка в презентации ради «вау-фактора» появилась. Типа: «вы все знаете, что нужно делать A, а не B — а догадываетесь ли вы, что иногда B — лучше?»
Догадываемся. Если бы всё программирование сводилось к набору простых правил, то на рынке не было бы такой острой нехватки программистов и они получали бы гораздо меньше. Но это не значит, что если вы придумали как сделать так, чтобы правило которое правильно срабатывает 1000 случаях из 1000, в одном случае из миллиона всё-таки «сломалось», то об этом нужно сразу на лекции рассказывать. Может «плохо подействовать на неокрепшие умы». Вероятность столкнуться с тем, что для итератора it++ вызовет замедление на порядок (а то и на два) превышает вероятность того, что код с a[i++] окажется хоть где-то быстрее, чем код с a[++i] — и это ещё если забыть что во многих случаях переписав этот код с использованием SSE/AVX/etc вы часто сможете получить экономию не в 0.05%, а в 50% ;-)hellamps
16.04.2016 16:17Да, про DMA я сморозил, конечно.
Про итераторы полностью согласен.
В целом, эта магия постфикса идет с этих типовых функций типа *strdest++ = *strsrc++, которые транслировались в movs, думаю так.
IvaYan
15.04.2016 17:18+2Если использовать i++ то сначала будет создана копия i, сдвинута на 1 позицию и присвоена обратно i. Если использовано ++i, то копии не создается.
vladon
16.04.2016 01:01-1только применяйте это только к сложным объектам, с обычными указателями это будет пессимизацией
ну и насчет создания копии можно уже поспорить, вроде бы clang уже умеет распознавать и оптимизировать подобноеkhim
16.04.2016 02:40+2только применяйте это только к сложным объектам, с обычными указателями это будет пессимизацией.
Не будет это пессимизацией, не надо вводить людей в заблуждение.
ну и насчет создания копии можно уже поспорить, вроде бы clang уже умеет распознавать и оптимизировать подобное
Вы либо штаны наденьте, либо крестик снимите… Если код доступен, то любой компилятор вам всё, что угодно отлично прооптимизирует и разницы между i++ и ++i не будет вообще. А если нет — то нет… собственно достаточно взглянуть на прототипы: преикремент может вернуть ссылку на итератор, а вот постинкремент — вынужден оный итератор копировать.
Так что правило простое — и именно такое, как написано: используйте для итераторов префиксный оператор инкремента (++i) вместо постфиксного (i++) причём даже тогда, когда эти итераторы — простые указатели или числа. Точка. Конец. Без исключений.
С процессорами и компиляторами где бы это простое правило давало сбой вам, скорее всего, встретится не удастся, а более сложное правило (используйте постинкремент, но предекремент!), которое когда-то, много лет назад имело смысл — настолько криво и неестественно, что, честное слово… оно того не стоит.
myrov_vlad
15.04.2016 17:21В случае постфиксного инкремента создается временный объект, используется, и только после увеличивается значение самой переменной, при работе с префиксным инкрементом временный объект не создается и переменная увеличивается сразу, что позволяет сэкономить немного ресурсов в случае комплексных переменных (например, итераторов).
Но при работе с переменными простых типов (например, обычных целочисленных) следует пользоваться все равно постфиксным инкрементом (т.к. отсутствует зависимость данных, подробнее habrahabr.ru/company/mailru/blog/279449/#comment_8809081 )Einherjar
15.04.2016 17:27-3Экономия ресурсов на операторах инкремента это конечно лол.
Надо полагать авторы имели ввиду что теоретически могут быть косяки из за незнания приоритета операторов если инкремент внутри сложного выражения, это вроде нередкая ошибка. Но имхо инкремент/декремент гораздо нагляднее всегда отдельным выражением делать а не запихивать в какие то сложные выражения. Это во-первых куда читаемее, а во-вторых постфиксный оператор хотя бы не так мерзко выглядит как префиксный.Andrey2008
15.04.2016 17:30+1Разница есть:
Einherjar
15.04.2016 17:34По вашим же ссылкам в Release разницы как раз никакой. А в дебаге… ну что то мне подсказывает что это мало где будет критичным боттлнеком.
Andrey2008
15.04.2016 17:41Скорость работы Debug версии не менее важна, чем Release. Да, конечному пользователю всё равно. А вот программисту нет.
vladon
16.04.2016 01:02-1хочу поспорить: измерять бенчмарк дебажной версии не имеет никакого практического смысла
Andrey2008
16.04.2016 09:37Бенчмарк — действительно смысла не имеет. Незачем сравнивать скорость работы отладочных версий.
Но для программистов скорость работы отладочной версии имеет большой смысл. И ускорить её хотя-бы на 10% бывает очень полезно и приятно. Я думаю, Вы просто не сталкивались с ресурсоёмкими приложениями и долгими тестами.
Whiteha
15.04.2016 19:34+4Что-то мне подсказывает что везде где не надо писать i++ это просто неосознанная привычка оставшаяся с первых проб в программировании. Да в релизе это не скажется, но в коде это показывает неосознанность автора и действительно замедляет циклы в и без того медленном дебаге, в котором все разработчики проводят под 95% времени.
vladon
16.04.2016 01:03-2ваши дебажные шаги все равно медленнее любого цикла
mihaild
16.04.2016 01:13Что такое «дебажные шаги»?
(ну и хотите я вам напишу код, который в 100 раз ускоряется что в дебаге что в релизе при замене постинкремента преинкрементом?)vladon
16.04.2016 01:16-1из скорости дебажной версии вообще не следует ничего о скорости релизной
более того, нет никакого практического смысла бенчмаркить и оптимизировать скорость работы дебажной версии
шаги в смысле шаги пошаговой отладкиmihaild
16.04.2016 05:26+1>из скорости дебажной версии вообще не следует ничего о скорости релизной
правда
>более того, нет никакого практического смысла бенчмаркить и оптимизировать скорость работы дебажной версии
неправда
>шаги в смысле шаги пошаговой отладки
>ваши дебажные шаги все равно медленнее любого цикла
Сформулируйте точнее: что именно медленнее чего? (какие отрезки времени больше каких?)
mihaild
15.04.2016 19:51+2Воообще и в релизе может быть разница. Редко, но бывают ситуации, когда итератор тяжелый и копировать его дорого.
niya3
15.04.2016 22:42+1На эту тему высказались многие из великих: Мейерс (6. «Различайте префиксную форму операторов инкремента и декремента»), Саттер(6. «Временные объекты» )+Александреску(9. «Не пессимизируйте преждевременно», 28. «Предпочитайте канонический вид ++ и --, и вызов префиксных операторов»), Страуструп+Cline( isocpp.org/wiki/faq/operator-overloading#increment-pre-post-speed ).
vladon
16.04.2016 01:08-2Без бенчмарков релизной версии всё это лишь слова, основанные на интуиции. Об этом, кстати, и говорит тот же Александреску.
khim
16.04.2016 02:58+2Давайте сведём всё, что вы тут понаписали вместе, идёт?
только применяйте это только к сложным объектам, с обычными указателями это будет пессимизацией
современные компиляторы умеют оптимизировать и не такое
из скорости дебажной версии вообще не следует ничего о скорости релизной
без бенчмарков релизной версии всё это лишь слова, основанные на интуиции
Собрав всё это воедино получаем: вы можете легко написать программу, которая использует обычные указатели и, будучи собрана современным компилятором в релизной версии, работает быстрее при использовании постинкремента.
Либо приведите пример подобной программы, либо заткнитесь и прекратите морочить людям голову.
niya3
16.04.2016 07:45+2Оптимизировать можно только после профилировщика, я помню. Но при этом:
Передача параметров по ссылке (рекомендация 25), использование префиксной формы операторов ++ и -- (рекомендация 28) или подобных идиом, которые при работе должны естественным образом "стекать с кончиков ваших пальцев", преждевременной оптимизацией не являются. Это всего лишь устранение преждевременной пессимизации (рекомендация 9)."
alexxx_b
15.04.2016 17:06-2По первому пункту так и просится цикл, но как автор кода, так и ваш анализатор это не видят.
Andrey2008
15.04.2016 17:08+1А причём здесь анализатор? Анализатор ищет ошибки. И кое-что на тему неэффективного кода. Но здесь код как раз руками оптимизированный (развернутый), хоть и неправильный.
Ошибку анализатор нашел. Больше ему здесь сказать нечего.
Vladusch
15.04.2016 17:17Т.к. Read-and-Comment, то могу только тут: Ссылка на англ. версию PDF на Я.Диске содержит русскую версию
heleo
15.04.2016 17:20А что мешало разместить книгу на Хабре, даже в виде набора статей, как основном источнике и тут же править по замечаниям в каментах? Как мне кажется, разбиение текста на малые блоки с обсуждением каждого по отдельности более эффективно, нежели сборка всего в одну кучу и отделение обрабатываемого ресурса от источника основного фидбэка.
Andrey2008
15.04.2016 17:20+1Когда материал побит на части, их сложно продвигать (рекламировать). У меня на этот материал большие планы в плане рекламы PVS-Studio. И мне хочется, чтобы всё было в одном месте.
Считайте это моими причудами. Я вот так захотел сделать и сделал. :)heleo
15.04.2016 18:25Думал про это как причину, но не думал что она основная. Не знаю как в правилах с рекламой на Хабре, но куча статей с ссылками на PVS-Studio, да ещё с большим банером в начале и конце, меня бы более заинтересовало со стороны маркентинга. А так на сайте после второй части уже ничего не маячит, аж до самого подвала, что могло бы о ней напоминать. Да и ссылка на Яндекс.Диск тоже в этом не способствует. Хотя если Вам нужен трафик на ресурс и счётчики… тогда другой вопрос))
Andrey2008
15.04.2016 20:58Мне нужны продажи. Способ достижения цели — интересный материал, в котором присутствует информация о PVS-Studio. Продвигать такой материал, знаю по опыту проще, когда он не побит на части. Иначе все равно приходится делать страницу «агригатор». Только хуже получается. Вот как-то так.
Krypt
15.04.2016 17:38+5> 11. Не жадничайте на строчках кода
В рамочку и на стену, вне зависимости от языка программирования. Код некоторых товарищей выглядит так, как будто их штрафуют за каждый лишний пробел или перенос строки.alexws54tk
15.04.2016 21:16Только не говорите этим IOCCC (http://www.ioccc.org/) ребятам.
Krypt
15.04.2016 21:56+2В принципе, пока они не коммитят код в мой проект, я ничего не имею против :)
Adamantium
15.04.2016 19:21Интересная подборка получилась, как и почти всегда Ваши статьи, спасибо.
Не знаю, где ещё спросить, но ответа не нашёл, а может просто плохо искал… Умеет ли ваш анализатор проверять и файлы с «почти си» кодом, в частности *.cu файлы в проектах, использующих CUDA? И если нет, то планируется ли это добавить в дальнейших релизах?Andrey2008
15.04.2016 21:00Быть может что-то и проверится, но никакой гарантии дать не могу. Поддержка CUDA не планируется. Однако, вдруг Вы сможете сделать предложение, от которого мы не сможем отказаться… Но это уже стоит обсуждать в почте. :)
an9eldust
15.04.2016 20:23PDF-версии очень недостает оглавления.
x_sourer
15.04.2016 22:05Поддерживаю по поводу активного оглавления в PDF.
ashcan
16.04.2016 00:13На 23 странице функция IsInterestinError вызывается без аргумента. Это не опечатка?
AndAm
16.04.2016 00:48C 4 пунктом не согласен. При грамотном использовании очень удобный оператор. Например для быстрой вставки по условию.
alexws54tk
16.04.2016 11:04С чем именно в 4 пункте вы не согласны?
Тоже думал было написать Автору про тернарный оператор, но потом прочёл сам текст, и там всё грамотно указано на приоритеты и на рекомендацию обрамления скобками, даже если выражение тривиально. Лично я не вижу смысла экономить на скобках в логических операциях.
sunman
16.04.2016 11:12-1Спасибо за труд, узнал много нового.
Но вот это место, похоже, само требует статического анализатора:
«if (headerM != 0) Адрес массива проверяется на равенство 0. Сравнение не имеет смысла, так как результат всегда true.»
Конечно же имелось в виду «проверяется на НЕравенство 0»
Mercury13
16.04.2016 12:06Насчёт DllMain. У меня на определённой машине LoO наглухо зависал при загрузке. Если выбить и запустить по новой — порядок. Не по этой ли причине?
herr_kaizer
16.04.2016 14:45[blockquote]Бойтесь printf[/blockqoute]
А чего в ней страшного?khim
16.04.2016 16:26Если короткое — то строка формата. Очень много подводных камней, в том числе потенциальных дыр в безопасности.
К сожалению у C++ stream'ов есть другая проблема — отсутствие строки формата. В результате — куча проблем с локализацией (в действительности программы, использующие потоки почти невозможно локализовывать).
У boost::format — всё вроде хорошо, обе проблемы более-менее решены, но… это нестандарт.
В результате на практике, если нет возможности использовать boost, то приходится использовать printf и потоки: printf — для надписей, которые должен видеть пользователь (и которые кто-то когда-то может решить локализовать), а потоки — для всего остальное (в частности для логов).
Так и живём…4144
17.04.2016 08:36Для форматной строки лучше всегда использовать константную строку.
Тогда компилятор выдаст предупреждение, если указаны неверные параметры. По крайней мере gcc такое умеет.khim
17.04.2016 17:22Если у вас можно использовать константную строку, то и iostream отлично справится.
Я говорю о чём-нибудь типа
понятно, что в идеале вам нужно грамотно обработать множественные числа и прочее, но даже с таким «расхлябанным» кодом переводчик может выкрутится и написать что-нибудь типаprintf(_("There are %d file(s) in directory \"%s\"!"), num, dir)
А в случае с iostream'ом форматной строки нет и отдельные «ошмётки», которые достанутся переводчику почти невозможно перевести так, чтобы результат не выглядет ужасно."В каталоге \"%2$s\" файлов: %1$d!"
Но если в примере выше переводчик просто переставит местами %s и %d — то программа и упасть может и даже уязвимость получить!
От всего этого спасает boost::format, но… нестандарт, увы.4144
17.04.2016 18:29Для корректных переводов надо использовать сервисы, которые не дают такое делать.
Или какую-ли утилиту. Не уверен что gettext может такое проверить, но там не случайно есть атрибуты c-format / no-c-format
yamatoko
16.04.2016 18:00-4а зачем нам на английском языке, да еще на ломанном?
Andrey2008
16.04.2016 18:02+1Основные продажи PVS-Studio — США и Европа.
vanxant
20.04.2016 10:31-4Так а на ломанном то зачем? Отдайте нативному спикеру на вычитку, это копейки стоит.
khim
20.04.2016 11:31+3От «нативного спикера» без IT-образования толку будет мало, скорее вред. А вычитывать всё, как с книгами делается, через несколько кругов редактирования прогонять — так статья через два года выйдет.
Mixim333
17.04.2016 17:49Хоть использую не C/C++, а C#, но для образовательных целей решил почитать.
Насчет постфиксных и префиксных операторов уже пару раз читал, но объективно, мне проще в цикле for инкрементировать переменную i с помощью i++, чем ++i, т.к. читается легче. Но применение цикла for стал очень ограничивать, если мне не нужно изменять иттерируемые объекты, то использую foreach.
«Есть масса упрямых программистов, которая не хочет видеть ничего опасного в сдвигах отрицательных чисел, переполнении знаковых чисел, сравнивании this c нулём и так далее.» — на дух не переношу сдвиги, всегда думал, что это со мной «что-то не так», а оказывается Вы их тоже не рекомендуете.
«Самое простое, но не очень удачное решение, это делать везде „default:“, который будет уведомлять об ошибке.» — C# здесь более строгий, если «default» нет и вхождение значения не найдено, то он выкинет исключение. На институтской скамье, после Pascal и C++ думал, что генерация исключения в таком случае — это дурость C#, пока в один прекрасный момент уже на работе эта «дурость» ни сохранила мне пару миллионов нервных клеток.
«И упаси вас боже, обвинять компилятор, что он неправильно собирает вашу программу. Такое конечно бывает, но крайне редко.» — а с вот этим не соглашусь. Писал в VS2012 код, который должен был обрабатывать CSV-файлы немалых объемов (сейчас этот объем составляет около 200Гб в сутки), использовать рефлексию(которую заменил на LINQ): запустил в Debug-режиме, подсунул файл «1.csv» — все отработало ровно так, как я и ожидал; откомпилировал приложение в Release, разместил на сервере, оно утром скачало тот же самый «1.csv», начало разбирать, произошло исключение, которое я залогировал; собрал приложение снова в Debug, подсунул скаченный файл, на котором произошла ошибка — все успешно разобралось; плюнул, случайно собрал приложение в Release, запустил на своей машине и оно упало ровно на том месте, на котором упало на сервере; запустил в Debug — снова все хорошо; понял, что проблема именно в разных версиях сборки; потом с другими целями поменял версию Framework'а, отвлекли, случайно поставил версию на Release, подсунул тот же самый файл и все отработало успешно. Так досконально и не понял, с чем был связан баг — то ли с Framework'ом, то ли еще с чем.
Да, спасибо за статью, было интересно почитатьvlivyur
18.04.2016 10:09+1В C# префиксный и постфиксный операторы одинаковы по времени и количеству операций.
khim
18.04.2016 13:49«Есть масса упрямых программистов, которая не хочет видеть ничего опасного в сдвигах отрицательных чисел, переполнении знаковых чисел, сравнивании this c нулём и так далее.» — на дух не переношу сдвиги, всегда думал, что это со мной «что-то не так», а оказывается Вы их тоже не рекомендуете.
Вопрос не в сдвигах. Грамотно применённые сдвиги — очень даже полезны. И опасны сдвиги не отрицательных чисел, конечно, а сдвиги на отрицательную величину.
Так досконально и не понял, с чем был связан баг — то ли с Framework'ом, то ли еще с чем.
Если вы досконально не понимаете с чем столкнулись, то о чём спорить-то? С вероятностью 99% в вашей программе до сих пор сидит ошибка, просто «ворошение кода» вокруг неё скрыло её из глаз.
Я сталкиваюсь с багами разных компиляторов и стандартных библиотек регулярно — то есть примерно обнаруживаю один-два-три бага в год. Сколько за это время я сажаю (и исправляю) багов у себя — сказать сложно, но речь явно идёт о сотнях, если не о тысячах.
Так что совет такой: если уж совсем всё работает «странно», то можно и баг компилятора заподозрить, они реально существуют, они не миф — но перед этим всё-таки лучше поискать ошибку у себя потщательнее ибо ошибка с вероятностью 99% — всё-таки где-то у вас в коде…
Andrey2008
18.04.2016 14:17Поправка.
Опасны сдвиги не отрицательных чисел, конечно, а сдвиги на отрицательную величину.
Нельзя осуществлять сдвиг отрицательных чисел. Это неопределённо поведение. http://www.viva64.com/ru/b/0142/khim
18.04.2016 19:08Прочитал стандарт. Слава богу то, что требуется (ASR) они оставили в покое. И то хлеб. А сдвиг влево… тут не очень даже и понятно, что, собственно, должно получиться, так что да, тут вы правы.
Andrey2008
18.04.2016 14:19+1«И упаси вас боже, обвинять компилятор, что он неправильно собирает вашу программу. Такое конечно бывает, но крайне редко.» — а с вот этим не соглашусь. Писал в VS2012 код, который должен был обрабатывать CSV-файлы немалых объемов (сейчас этот объем составляет около 200Гб в сутки), использовать рефлексию(которую заменил на LINQ): запустил в Debug-режиме, подсунул файл «1.csv» — все отработало ровно так, как я и ожидал; откомпилировал приложение в Release, разместил на сервере, оно утром скачало тот же самый «1.csv», начало разбирать, произошло исключение, которое я залогировал; собрал приложение снова в Debug, подсунул скаченный файл, на котором произошла ошибка — все успешно разобралось; плюнул, случайно собрал приложение в Release, запустил на своей машине и оно упало ровно на том месте, на котором упало на сервере; запустил в Debug — снова все хорошо; понял, что проблема именно в разных версиях сборки; потом с другими целями поменял версию Framework'а, отвлекли, случайно поставил версию на Release, подсунул тот же самый файл и все отработало успешно. Так досконально и не понял, с чем был связан баг — то ли с Framework'ом, то ли еще с чем.
Не понял в чём дело == виноват компилятор.
Кхм… Что-то не очень доказательство. :)
Hawker
17.04.2016 18:36Рекомендация 31:
Программисты иногда забывают, что в C/C++ нельзя передать в функцию массив по значению. На самом деле, в качестве аргумента передается указатель на массив. Числа в квадратных скобках ничего не значат, они всего лишь служат, своего рода подсказкой программисту, массив какого размера предполагается передать.
Для многомерных массивов это не так.
В С/С++ многомерные массивы хранятся последовательно, для вычисления адреса элемента компилятору нужно знать все размерности, кроме первой. Ее-то и можно проигнорировать.
void foo(int x[][]) // ошибка компиляции
void foo(int x[][3]) // ok
ainoneko
18.04.2016 08:18В 17-м пункте в первом из вариантов корректного кода (
memset_s(x, sizeof(apr_uint32_t), 0, sizeof(APR_MD4_DIGESTSIZE));
) мне кажется подозрительным «sizeof».
fareloz
18.04.2016 17:1738. С сегодняшнего дня используйте nullptr вместо NULL
У меня всегда возникает проблема с функциями WinApi. Уместно ли туда передавать nullptr? Я всегда считал, что нужно использоваться средства библиотеки при работе с этой библиотекой. И когда появился nullptr каждый раз возникает трудный выбор что передать. Особенно когда есть пример из MSDN с NULL вместо аргумента-указателяkhim
18.04.2016 19:14+1Пример из MSDN с вероятностью 90% был написан до того, как nullptr вообще появился. Какой может быть «трудный выбор» — мне, если честно, неясно.
Правило, на самом деле, простое: NULL — не нужно использовать. Никогда. Вообще. Точка.
Если ОЧЕНЬ НАДО поддерживать старые компиляторы — компилируйте с опцией -Dnullptr=NULL или аналогичной.
Либо библиотека принимает nullptr — и тогда нужно туда передать nullptr, либо не принимает — тогда нужно связаться с разработчиком и выяснить, с какого перепугу у него написано в документации, что куда-то нужно передавать NULL, хотя там явно требуется int. В 100 случаев из 100 это ошибка. Причём в 90% — в программе (и её нужно исправить), в 10% — в документации (и её нужно исправить тоже).
webkumo
20.04.2016 02:3118. Знания, полученные при работе с одним языком, не всегда применимы к другому языку
я бы предположил, что автор предложенного вами кода не бывшый паскалист, а например разработчик Java: в Java условия выхода из цикла тоже проверяется каждый раз, но сама архитектура строки другая — там это просто обращение к свойству объекта… аналогичная ситуация есть и во многих других языках. Впрочем сам-то совет полностью верный, только нужно уточнить, что внимание нужно обратщать как на синтаксические конструкции так и на реализацию данных…
iSage
Как мило. «Предвижу. Заранее выкладываю». Экий вы предвидец. Особенно после ЛОР-эффекта и просьбы выложить в пдф.