Это вторая статья о том, как можно избежать ряда ошибок еще на этапе написания кода. В предыдущей заметке уже упоминался совет избегать множества вычислений в одном выражении. Однако, этот вопрос требует более пристального внимания. Рассмотрим опасность сложных условий, и как можно предупредить многие логические ошибки.
Как уменьшить вероятность ошибки на этапе написания кода. Заметка N2.
1. Как уменьшить вероятность ошибки
на этапе написания кода. Заметка N
N2.
Автор: Андрей Карпов
Дата: 29.03.2011
Аннотация
Это вторая статья о том, как можно избежать ряда ошибок еще на этапе написания кода. В
предыдущей заметке уже упоминался совет избегать множества вычислений в одном выражении.
Однако, этот вопрос требует более пристального внимания. Рассмотрим опасность сложных
условий, и как можно предупредить многие логические ошибки.
Введение
С предыдущей заметкой можно познакомиться здесь. В этот раз примеры ошибок будут взяты из
т
различных известных проектов, чтобы подчеркнуть их распространенность Ошибки, которые
распространенность.
будут продемонстрированы были найдены мной с помощью анализатора PVS-Studio за
продемонстрированы,
относительно большой промежуток времени. Практически про все я писал разработчикам и
поэтому можно надеяться, что они поправлены в новых редакциях кода. Это я пишу во введении,
потому что после статей мне всегда приходят письма с просьбой "напишите про найденные вами
ошибки разработчикам проекта".
1. Не используйте тернарн
тернарную операцию '?:' в составных выражениях
Тернарная условная операция записывается в языке Си/Си++ с помощью оператора '?:'. Операция,
Си++
возвращающая свой второй или третий операнд в зависимости от значения логического
выражения, заданного первым операндом. Пример:
int minValue = A < B ? A : B;
Тернарная операция имеет очень низкий приоритет (см. таблицу). Про это часто забывают и
именно из-за этого тернарная операция крайне опасна.
за
2. Рисунок 1 - Операции языка Си/Си++, в порядке снижения приоритета
Обратите внимание, что операция '?:' имеет более низкий приоритет, чем сложение, умножение,
оператора побитового ИЛИ и так далее. Рассмотрим код:
int Z = X + (A == B) ? 1 : 2;
Этот код работает не так, как может показаться на первый взгляд. Программист, скорее всего,
хотел сложить значение X с числом 1 или с числом 2, в зависимости от условия (A == B). Но на
самом деле условием является выражение "X + (A == B)". Фактически, здесь написано:
int Z = (X + (A == B)) ? 1 : 2;
А хотелось:
3. int Z = X + (A == B ? 1 : 2);
Первая мысль – так ведь надо знать приоритет операций. А программисты их и знают, но уж очень
коварна эта тернарная операция! Ошибки с ней допускают не только новички, но и матёрые
программисты. Её нередко можно встретить даже в самом качественном коде. Вот пара
примеров.
V502 Perhaps the '?:' operator works in a different way than it was expected. The '?:' operator has a
lower priority than the '*' operator. physics dgminkowskiconv.cpp 1061
dgInt32 CalculateConvexShapeIntersection (...)
{
...
den = dgFloat32 (1.0e-24f) *
(den > dgFloat32 (0.0f)) ?
dgFloat32 (1.0f) : dgFloat32 (-1.0f);
...
}
V502 Perhaps the '?:' operator works in a different way than it was expected. The '?:' operator has a
lower priority than the '-' operator. views custom_frame_view.cc 400
static const int kClientEdgeThickness;
int height() const;
bool ShouldShowClientEdge() const;
4. void CustomFrameView::PaintMaximizedFrameBorder(gfx::Canvas* canvas) {
...
int edge_height = titlebar_bottom->height() -
ShouldShowClientEdge() ? kClientEdgeThickness : 0;
...
}
V502 Perhaps the '?:' operator works in a different way than it was expected. The '?:' operator has a
lower priority than the '|' operator. vm vm_file_win.c 393
#define FILE_ATTRIBUTE_NORMAL 0x00000080
#define FILE_FLAG_NO_BUFFERING 0x20000000
vm_file* vm_file_fopen(...)
{
...
mds[3] = FILE_ATTRIBUTE_NORMAL |
(islog == 0) ? 0 : FILE_FLAG_NO_BUFFERING;
...
}
Как видите, ошибки данного типа заслуживают внимания. И я не случайно выделил их описание в
отдельный пункт. Они весьма и весьма распространены. Можно привести другие примеры, но все
они однотипны.
Избежать подобных ошибок можно, если не стремиться поместить несколько операций в одну
строку кода. А если и помещать, то не жадничать поставить дополнительные скобки. Про скобки
мы поговорим ниже. Сейчас попробуем просто предотвратить потенциальные ошибки при
использовании '?:'.
Конечно оператор '?:' является синтаксическим сахаром и его практически всегда можно заменить
на if. К редким исключениям относится такие задачи, как инициализация ссылки:
MyObject &ref = X ? A : B;
5. Конечно, и с этим проблем нет, но создание ссылки на A или B без оператора '?:' будет гораздо
многословней:
MyObject *tmpPtr;
If (X)
tmpPtr = &A;
else
tmpPtr = &B;
MyObject &ref = *tmpPtr;
Итак, отказываться от оператора '?:' не рационально. Но и ошибиться с ним просто. Поэтому лично
я выработал для себя следующее правило. Результат '?:' должен сразу куда-то помещаться и не
сочетаться с другими действиями. То есть слева от условия оператора '?:' должна стоять операция
присваивания. Вернемся к первоначальному примеру:
int Z = X + (A == B) ? 1 : 2;
Я предлагаю писать так:
int Z = X;
Z += A == B ? 1 : 2;
В случае примера, относящегося к IPP Samples, я бы написал так:
mds[3] = FILE_ATTRIBUTE_NORMAL;
mds[3] |= (islog == 0) ? 0 : FILE_FLAG_NO_BUFFERING;
С рекомендацией можно не согласиться. Не буду её отстаивать. Например, мне не очень нравится,
что вместо одной строки мы получаем две или более. Неплохой альтернативой может быть
обязательное взятие оператора '?:' в скобки. Я ставлю основной задачей показать паттерны
ошибок. Выбор паттерна защиты от ошибок зависит от предпочтений программиста.
2. Не стесняйтесь использовать скобки
Почему-то так повелось, что использование лишних скобок при программировании на Си/Си++
считается неким постыдным действием. Возможно, это идёт от того, что вопрос про приоритет
операций очень любят задавать на собеседовании. И у человека подсознательно откладывается
стремление всегда максимально использовать механизм приоритетов. Ведь если он поставит
лишние скобки, то вдруг кто-то подумает что он новичок, а не истинный джедай.
Я даже встречал на просторах интернета обсуждение, где человек категорично утверждал, что
использование лишних скобок - это плохой тон. А если человек не уверен как будет вычислено
выражение, то ему надо учиться, а не программы писать. К сожалению, не смог найти эту
дискуссию, но я не разделяю подобные точки зрения. Знать приоритеты, конечно, надо, но если в
выражении используются разнородные операции, то лучше подстраховывать себя скобками. Это
6. не только защитит от потенциальных ошибок, но и сделает код более легко читаемым для других
разработчиков.
Ошибки с путаницей приоритетов также возникают не только у новичков, но и у профессионалов.
При этом выражение не обязательно должно быть сильно запутанным и длинным. Ошибку можно
допустить и в относительно простых выражениях. Рассмотрим некоторые примеры.
V564 The '&' operator is applied to bool type value. You've probably forgotten to include parentheses or
intended to use the '&&' operator. game g_client.c 1534
#define SVF_CASTAI 0x00000010
char *ClientConnect(...) {
...
if ( !ent->r.svFlags & SVF_CASTAI ) {
...
}
V564 The '&' operator is applied to bool type value. You've probably forgotten to include parentheses or
intended to use the '&&' operator. dosbox sdlmain.cpp 519
static SDL_Surface * GFX_SetupSurfaceScaled(Bit32u sdl_flags,
Bit32u bpp) {
...
if (!sdl.blit.surface || (!sdl.blit.surface->flags&SDL_HWSURFACE)) {
...
7. }
Ну и еще один пример из Chromium:
V564 The '&' operator is applied to bool type value. You've probably forgotten to include parentheses or
intended to use the '&&' operator. base platform_file_win.cc 216
#define FILE_ATTRIBUTE_DIRECTORY 0x00000010
bool GetPlatformFileInfo(PlatformFile file, PlatformFileInfo* info) {
...
info->is_directory =
file_info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0;
...
}
Выражения просты. Разработчики молодцы. А ошибки всё равно есть. Так что использование
скобок в скользких местах лишним не будет.
Наверное, стоит поступать так. Если операции просты и привычны, то дополнительные скобки не
нужны. Примеры:
if (A == B && X != Y)
if (A - B < Foo() * 2)
А вот если используются более редкие операторы (~, ^, &, |, <<, >>, ?:), то явные скобки не
повредят. Это и упростит чтение кода и защит от потенциальной ошибки. Примеры:
If ( ! (A & B))
x = A | B | (z < 1 ? 2 : 3);
Не стесняться использовать скобки при использовании редких операций поможет и для
рассмотренного ранее оператора "?:". Как именно лучше поступить с "?:" - дело вкуса. Мне более
нравится вариант с упрощением.
Заключение
Пишите простой и понятный код. Разбивая длинные и сложные выражения на несколько строк, вы
получаете более длинный код. Но такой код бывает намного проще понять и прочитать. В нем
меньше вероятность допустить ошибку. Не бойтесь завести лишнюю переменную. Компилятор
отлично всё оптимизирует.
Не жадничайте на скобках в выражениях, где используются редкие операторы или где
смешиваются битовые и логические операции.
8. Программист, который в будущем будет читать ваш код со скобками, только скажет спасибо за
это.