Вебинар: C++ и неопределённое поведение - 27.02
Форматирование кода таблицей ("табличное" оформление кода) помогает сделать текст программы простым для чтения и восприятия. Это, в свою очередь, упрощает проведение обзоров кода и выявление ошибок и опечаток.
Рассмотрим фрагмент кода из проекта ReactOS, в котором мы обнаружили ошибку благодаря предупреждению PVS-Studio: V560 A part of conditional expression is always true: 10035L.
void adns__querysend_tcp(adns_query qu, struct timeval now) {
....
if (!(errno == EAGAIN || EWOULDBLOCK ||
errno == EINTR || errno == ENOSPC ||
errno == ENOBUFS || errno == ENOMEM)) {
....
}
Здесь приведён маленький фрагмент кода. Найти в нём ошибку несложно, но в реальном коде заметить её весьма проблематично. Взгляд просто пропускает блок однотипных сравнений и идёт дальше.
Почему разработчики пропускают такие ошибки? Условия плохо отформатированы, поэтому не хочется внимательно их читать, прикладывая усилия. Человек надеется, что раз проверки однотипные, то всё хорошо, и автор кода не допустил ошибок в условии.
Одним из способов борьбы с таки опечатками как раз и является "табличное" оформление кода.
Ошибка здесь в том, что в одном месте пропущено errno ==
. В результате условие всегда истинно, так как константа EWOULDBLOCK
равна 10035
. Корректный код:
if (!(errno == EAGAIN || errno == EWOULDBLOCK ||
errno == EINTR || errno == ENOSPC ||
errno == ENOBUFS || errno == ENOMEM)) {
Теперь рассмотрим, как лучше провести рефакторинг этого фрагмента. Для начала рассмотрим код, оформленный самым простым "табличным" способом.
if (!(errno == EAGAIN || EWOULDBLOCK ||
errno == EINTR || errno == ENOSPC ||
errno == ENOBUFS || errno == ENOMEM)) {
Стало лучше, но ненамного. Такой стиль оформления неудовлетворителен по двум причинам:
Поэтому надо сделать два усовершенствования в оформлении кода. Первое: не больше одного сравнения на строку, тогда ошибку легко заметить. Смотрите, в таком коде опечатка сразу бросится в глаза:
a == 1 &&
b == 2 &&
c &&
d == 3 &&
Второе: рационально писать операторы &&
, ||
и т. д. не справа, а слева.
Обратите внимание, как много работы для написания пробелов:
x == a &&
y == bbbbb &&
z == cccccccccc &&
Так работы намного меньше:
x == a
&& y == bbbbb
&& z == cccccccccc
Выглядит код немного необычно, но к этому быстро привыкаешь. Поэтому мы рекомендуем именно такой подход к форматированию кода.
Объединим это всё вместе и напишем в новом стиле код, приведённый в начале:
if (!( errno == EAGAIN
|| EWOULDBLOCK
|| errno == EINTR
|| errno == ENOSPC
|| errno == ENOBUFS
|| errno == ENOMEM)) {
Код стал занимать больше строк, зато ошибка стала заметнее. Увеличившееся количество строк вообще не является в чем-то плохим в этом случае. Лёгкость чтения и отсутствие опечаток куда важнее количества строк кода.
Можно продолжить рефакторинг:
const bool error = errno == EAGAIN
|| errno == EWOULDBLOCK
|| errno == EINTR
|| errno == ENOSPC
|| errno == ENOBUFS
|| errno == ENOMEM;
if (!error) {
Ещё один возможный шаг по улучшению — вынести код проверки в функцию:
static bool IsInterestingError(int errno)
{
return errno == EAGAIN
|| errno == EWOULDBLOCK
|| errno == EINTR
|| errno == ENOSPC
|| errno == ENOBUFS
|| errno == ENOMEM;
}
....
if (!IsInterestingError(errno)) {
В редких случаях форматирование "таблицей" может пойти во вред. Вот один из примеров:
inline
void elxLuminocity(const PixelRGBi& iPixel,
LuminanceCell< PixelRGBi >& oCell)
{
oCell._luminance = 2220*iPixel._red +
7067*iPixel._blue +
0713*iPixel._green;
oCell._pixel = iPixel;
}
Мы встретили этот код в проекте eLynx SDK. Программист хотел выровнять код, поэтому перед 713
дописал 0
. К сожалению, он не учёл, что 0
в начале числа означает, что число будет представлено в восьмеричном формате.
Форматирование таблицей можно применять не только к условиям, но и к совершенно разным конструкциям языка.
Фрагмент взят из проекта Asterisk. Ошибка выявляется диагностикой PVS-Studio: V653 A suspicious string consisting of two parts is used for array initialization. It is possible that a comma is missing. Consider inspecting this literal: "KW_INCLUDES" "KW_JUMP".
static char *token_equivs1[] =
{
....
"KW_IF",
"KW_IGNOREPAT",
"KW_INCLUDES"
"KW_JUMP",
"KW_MACRO",
"KW_PATTERN",
....
};
Опечатка — забыта запятая. В результате две различные по смыслу строки соединяются в одну, т.е. c точки зрения компилятора здесь написано:
....
"KW_INCLUDESKW_JUMP",
....
Ошибки можно было бы избежать, выравнивая код таблицей. Тогда, если запятая будет пропущена, это будет легко заметить.
static char *token_equivs1[] =
{
....
"KW_IF" ,
"KW_IGNOREPAT" ,
"KW_INCLUDES" ,
"KW_JUMP" ,
"KW_MACRO" ,
"KW_PATTERN" ,
....
};
Как и в прошлый раз, обращаю внимание, что если мы ставим разделитель справа (в данном случае это запятая), то приходится добавлять массу пробелов, что неудобно. Особенно неудобно, если появляется новая длинная строка/выражение — придётся переформатировать всю таблицу.
Поэтому вновь актуальна рекомендация оформлять код так:
static char *token_equivs1[] =
{
....
, "KW_IF"
, "KW_IGNOREPAT"
, "KW_INCLUDES"
, "KW_JUMP"
, "KW_MACRO"
, "KW_PATTERN"
....
};
Теперь очень легко заметить недостающую запятую и нет необходимости писать много пробелов — код красивый и понятный. Оформление поначалу кажется странным, но к нему быстро привыкаешь, попробуйте.
0