Название: Как уменьшить вероятность ошибки на этапе написания кода. Заметка N3. Отправлено: Andrey2011 от Июль 13, 2011, 09:34 Аннотация
Это третья статья, где я хочу рассказать про новую пару приёмов при программировании, которые помогут сделать код более простым и надежным. С предыдущими двумя заметками можно познакомиться здесь (http://www.viva64.com/ru/a/0070/) [1] и здесь (http://www.viva64.com/ru/a/0072/) [2]. В этот раз примеры будут взяты из проекта Qt. Введение Проект Qt 4.7.3. попался мне для изучения не случайно. Пользователи PVS-Studio обратили внимание, что анализ как-то слабоват, если дело касается проверки проектов, построенных на основе библиотеки Qt. Это не удивительно. Статический анализ позволяет находить ошибки за счет того, что смотрит на код более высокоуровнево, чем компилятор. Следовательно, он должен знать определенные паттерны кода и, что делают функции различных библиотек. В противном случае, он пройдет мимо многих замечательных ляпов. Поясню на примере: Код: if (strcmp(My_Str_A, My_Str_A) == 0) Бессмысленно сравнивать строку саму с собой. Но компилятор промолчит. Он не задумывается, в чем суть функции strcmp(). У него хватает своих забот. А вот статические анализаторы могут заподозрить неладное. В Qt есть своя разновидность функции сравнения строк - qstrcmp(). И, соответственно, анализатор нужно обучить обращать внимание и на такую строку: Код: if (qstrcmp(My_Str_A, My_Str_A) == 0) Осваивание библиотеки Qt и создание специализированных проверок - это большая и планомерная работа. И началом этой работы стала проверка самой библиотеки. Закончив просмотр предупреждений, у меня созрело несколько новых мыслей по улучшению кода, которые, как я надеюсь, будут вам интересны и полезны. 1. Обрабатывайте переменные в той же последовательности, как они объявлены Код библиотеки Qt очень качественен и практически не содержит ошибок. Зато в нём обнаружилось большое количество излишних инициализаций, излишних сравнений или лишних копирования значений переменных. Приведу пару примеров для ясности: Код: QWidget *WidgetFactory::createWidget(...) Здесь два раза продублировано одно и то же сравнение. Это не ошибка, но совершенно избыточный код. Другой аналогичный пример: Код: void QXmlStreamReaderPrivate::init() Вновь не ошибка, но совершенно ненужная двойная инициализация переменной. И подобных продублированных действий я насчитал очень много. Возникают они из-за длинных списков сравнений, присваиваний, инициализаций. Просто не видно, что переменная уже обрабатывается, из-за чего и появляются лишние операции. Я могу назвать три неприятных последствия наличия таких дублирующихся действий: 1. Дубликаты увеличивают длину кода. А чем длиннее код, тем легче добавить еще один дубликат. 2. Если мы захотим изменить логику программы и удалим одну проверку или одно присваивание, то дубликат этого действия может подарить вам несколько часов увлекательной отладки. Сами представьте, вы пишите "tos = 1" (см. первый пример), а потом в другой части программы удивляетесь, отчего же по-прежнему "tos" равен нулю. 3. Замедление скорости работы. Практически всегда им можно пренебречь в таких ситуациях, но оно всё-таки есть. Надеюсь, убедил, что дубликатам не место в нашем коде. Как с ними бороться? Как правило, подобные инициализации/сравнения идут блоком. И есть такой же блок переменных. Рационально писать код так, чтобы порядок объявлений переменных и порядок работы с ними совпадал. Приведу пример не очень хорошего кода: Код: struct T { Естественно, это схематичный пример. Смысл в том, что когда инициализация не последовательна, то намного легче написать две одинаковых строки. В приведенном коде дважды инициализируется переменная 'q'. И ошибка плохо заметна, если просматривать код бегло. Если теперь инициализировать переменные в той же последовательности, как они объявлены, то подобной ошибке просто не будет места. Улучшенный вариант кода: Код: struct T { Конечно, я знаю, что не всегда так можно написать и работать с переменными в порядке их объявления. Но часто это возможно и полезно. Дополнительным плюсом станет то, что вам легче будет ориентироваться в коде. Рекомендация. Добавляя новую переменную, постарайтесь, чтобы её инициализация и обработка, осуществлялась в соответствии с её положением относительно других переменных. 2. Табличные методы - это хорошо. Про табличные методы хорошо написано у С. Макконнелла в книге "Совершенный код" в главе N18 [3]: Табличный метод - это схема, позволяющая искать информацию в таблице, а не использовать для этого логические выражения, такие как if и case. Практически все, что вы можете выбирать посредством логических операторов, можно выбирать, применяя таблицы. В простых случаях логические выражения проще и понятней. Но при усложнении логических построений таблицы становятся всё привлекательнее. Так вот, очень жаль, что программисты по-прежнему любят огромные switch() или густой лес конструкций if-else. Перебороть в себе это очень сложно. Кажется, ну еще-то один "case:" или маленький "if" не повредит. Но он вредит. И неудачно добавляют новые условия даже самые опытные программисты. В качестве примера пара дефектов, найденных в Qt. Код: int QCleanlooksStyle::pixelMetric(...) Длинный-предлинный switch(). И, естественно, имеется забытый оператор "break". Анализатор выявил эту ошибку за счет того, что переменной "ret" дважды подряд присваивается разное значение. Пожалуй, намного лучше, было бы завести какой-то std::map<PixelMetric, int> и явно табличкой задать соответствие между метрикой и числами. Можно придумать и другие табличные варианты реализации функции. Еще один пример: Код: QStringList ProFileEvaluator::Private::values(...) В коде два раза сравниваем переменную 'ver' с константой WV_2000. Хороший пример, где самое место табличному методу. Например, такой метод мог бы выглядеть так: Код: struct { Конечно, это просто прототип, но он хорошо демонстрирует идею табличных методов. Согласитесь, что выявить в такой таблице ошибку намного проще. Рекомендация. Не ленитесь написать функцию с использованием табличных методов. Да, это займет немного времени, но зато это окупится потом. Вам будет проще и быстрее добавлять новые условия, а вероятность ошибки при этом будет намного ниже. 3. Разное интересное Поскольку Qt большая библиотека, то, несмотря на высокое качество, в ней можно встретить разнообразнейшие ошибки. Действует закон больших чисел. Размер *.cpp, *.h и аналогичных файлов проекта Qt составляет около 250 мегабайт. Как не маловероятна ошибка, в большом коде её встретить вполне реально. На основании других ошибок, которые я обнаружил в Qt, составить какие-то рекомендации сложно. Просто опишу некоторые ошибки, которые мне понравились. Код: QString decodeMSG(const MSG& msg) Случайно используется оператор && вместо &. Обратите внимание, как полезно иметь в коде комментарии. Сразу становится понятно, что это действительно ошибка и как на самом деле должны обрабатываться биты. Следующий пример будет на тему длинных выражений: Код: static ShiftResult shift(...) Видите ошибку? Вот-вот, с ходу и не заметишь. Хорошо, подскажу. Беда вот здесь: "orig->y1 - orig->y1". Ещё меня третье умножение смущает, но возможно так и надо. Да, еще вопрос. А у вас ведь тоже в программах вот такие вот блоки вычислений есть? Не пора ли попробовать статический анализатор кода PVS-Studio (http://www.viva64.com/ru/pvs-studio-download/)? Так, порекламировал. Пойдем дальше. Использование неинициализированных переменных. Их можно найти в любом большом приложении: Код: PassRefPtr<Structure> Здесь опять нужно дать подсказу, чтобы долго не мучить ваши глаза. Смотреть надо на инициализацию переменной "transition->m_hasGetterSetterProperties". Уверен, почти каждый из нас, когда только начинал программировать, делал ошибку в духе: Код: const char *p = ...; И только потом приходило осознание, зачем нужны такие на первый взгляд странные функции, как strcmp(). К сожалению, язык Си++ настолько суров, что сделать такую ошибку можно и через много лет, будучи профессиональным разработчиком с опытом: Код: const TCHAR* getQueryName() const; Что бы еще показать. Вот, например, неправильно написанный обмен значений переменных. Код: bool qt_testCollision(...) Это пример того, что можно ошибиться даже в очень простом коде. Так, пока ещё не было примеров на тему выхода за границы массива. Сейчас будет: Код: bool equals( class1* val1, class2* val2 ) const Условие "--size >= 0" всегда истинно, так как переменная size имеет беззнаковый тип. Если будут сравниваться одинаковые последовательности, то произойдет выход за границы массивов. Можно продолжать и дальше. Надеюсь, вы как программисты понимаете, что ошибки в проекте такого объема описывать в одной статье нет никакой возможности. Поэтому последнее, на закуску: Код: STDMETHODIMP QEnumPins::QueryInterface(const IID &iid,void **out)
Спасибо за внимание. Используйте статический анализ кода, и вы сможете сэкономить массу времени для более полезных дел, чем отладка и сопровождение кода. Также я буду благодарен читателям, если вы пришлете мне интересные примеры ошибок, которые вы находили в своём или чужом коде и, для которых можно реализовать диагностические правила. Библиографический список [list=1]
Название: Re: Как уменьшить вероятность ошибки на этапе написания кода. Заметка N3. Отправлено: Пантер от Июль 13, 2011, 09:45 1. Поприетарщина.
2. Все приведенное банально. Так что это просто реклама пункта 1. 3. Советую это все закинуть Нокии, чтобы они поправили свои ошибки. Название: Re: Как уменьшить вероятность ошибки на этапе написания кода. Заметка N3. Отправлено: LisandreL от Июль 13, 2011, 10:22 Советую это все закинуть Нокии, чтобы они поправили свои ошибки. http://habrahabr.ru/blogs/qt_software/123603/#comment_4059621Название: Re: Как уменьшить вероятность ошибки на этапе написания кода. Заметка N3. Отправлено: Igors от Июль 13, 2011, 10:43 1) Ну неважно с "саморекламой" - нет напористости и (в каком-то смысле) наглости. Тут на форуме есть специалисты которые умеют "себя подать" в 10 раз лучше хотя понимают в 10 раз меньше чем Вы.
2) На мой взгляд нет цельности изложения - Вы затронули интересный/важный момент: "таблички", как избегать длинных switch - ну и парили бы себе только эту тему, от этого статья бы только выиграла 3) Пожалуй, намного лучше, было бы завести какой-то std::map<PixelMetric, int> и явно табличкой задать соответствие между метрикой и числами. Можно придумать и другие табличные варианты реализации функции. Мне это хорошо знакомо и таблички бывают весьма развесистые. ПримерКод Конечно все это имеет смысл если таких "однотипных" листов достаточно много. Но так я получаю обвинение типа "это С с классами!". Это так, но какое лучшее решение? Завести std::map? Хмм.. а в какой момент его заполнить данными? Пусть это несложно, но все-таки не так удобно как объявить const массив - и все дела. Да и ключей мне нужно штук 5 - а там аж 1 Все это было бы интересно обсудить - конечно если есть желание :) Название: Re: Как уменьшить вероятность ошибки на этапе написания кода. Заметка N3. Отправлено: GreatSnake от Июль 13, 2011, 11:21 Цитата: Igors Но так я получаю обвинение типа "это С с классами!". Чего-то вы зациклились на этих "обвинениях". Чушь всё это.Для статической инициализации ничего компактнее, проще и нагляднее ещё не придумано. И не надо забывать, что язык называется C++. Название: Re: Как уменьшить вероятность ошибки на этапе написания кода. Заметка N3. Отправлено: Igors от Июль 13, 2011, 11:25 Чего-то вы зациклились на этих "обвинениях". Чушь всё это. Да я от этого никак не страдаю :) Просто делать это приходится довольно часто, вот и интересно как другие это решаютДля статической инициализации ничего компактнее, проще и нагляднее ещё не придумано. И не надо забывать, что язык называется C/C++. Название: Re: Как уменьшить вероятность ошибки на этапе написания кода. Заметка N3. Отправлено: brankovic от Июль 13, 2011, 12:07 когда подобные подборки вижу, всегда возникает ощущение их бессмысленности. Те, кому сказанное не очевидно, всё равно не будут применять, а те, кому очевидно, и так применяют. В общем "А если что-то надо объяснять, то ничего не надо объяснять.."
(только начало просмотрел, каюсь :)) Название: Re: Как уменьшить вероятность ошибки на этап& Отправлено: iks от Июль 14, 2011, 10:17 Есть хорошая поговорка в России
Цитировать Не ошибается тот, кто ни чего не делает Ну нашлись ляпы, так это гномам говорить надо о них сразу, есть такая ссылочка в Qt CreatorЦитировать Помогите нам сделать Qt Creator лудше Не помню где , даже вроде на этом форуме было написано о гениальном коде без багов и ляповКод Вы посмотрите какое изящество ни одной ошибки ведь нет =) Название: Re: Как уменьшить вероятность ошибки на этап& Отправлено: LisandreL от Июль 14, 2011, 10:50 ни одной ошибки ведь нет Цитировать ошибка: undefined reference to `qMain(int, char**)' Название: Re: Как уменьшить вероятность ошибки на этап& Отправлено: brankovic от Июль 14, 2011, 12:09 ни одной ошибки ведь нет Цитировать ошибка: undefined reference to `qMain(int, char**)' неправильно компилите, это либа |