Логическое выражение в программировании — конструкция языка программирования, результатом вычисления которой является «истина» или «ложь». Во многих книгах по программированию, предназначенных для изучения языка «с нуля», приводятся возможные операции над логическими выражениями, с которыми сталкивался каждый начинающий разработчик. В этой статье я не буду рассказывать, что оператор 'И' приоритетнее оператора 'ИЛИ'. Я расскажу о распространённых ошибках в простых условных выражениях, состоящих всего из трёх операторов, и покажу, как можно проверить свой код с помощью построения таблиц истинности. Описанные ошибки делают разработчики таких известных проектов как FreeBSD, Microsoft ChakraCore, Mozilla Thunderbird, LibreOffice и многих других.
Введение
Я занимаюсь разработкой статического анализатора кода для языков C/C++/C# — PVS-Studio. В моей работе приходится много сталкиваться с открытым и закрытым кодом разных проектов. Часто результатом такой работы являются статьи о проверке open source проектов, содержащие описание найденных ошибок и недочётов. После просмотра большого объёма кода начинаешь замечать различные паттерны ошибок, которые допускают программисты. Так, мой коллега Андрей Карпов писал статью про эффект последней строки после нахождения большого количества ошибок, допущенных в последних фрагментах однотипного кода.
В начале этого года я проверил с помощью анализатора много проектов крупных компаний в сфере IT, которые, следуя современной тенденции, выкладывают в открытый доступ исходный код своих проектов под свободными лицензиями. Я стал замечать, что почти в каждом проекте находится ошибка в условном выражении из-за неправильной записи условных операторов. Само выражение достаточно простое и состоит всего из трёх операторов:
- != || !=
- == || !=
- == && ==
- == && !=
Всего таких условных выражений можно записать 6 штук, но 4 из них являются ошибочными: два являются всегда истинным или ложным; в двух результат всего выражения не зависит от результата входящего в него подвыражения.
Для доказательства неверного результата выражения я буду строить таблицу истинности в каждом примере; также я приведу для каждого примера по одному фрагменту кода из открытого проекта. В этой статье будет упомянут и тернарный оператор '?:', который имеет почти самый низкий приоритет из всех операторов, но очень много разработчиков не знают об этом.
Т.к. чаще всего я встречал неправильные условные выражения при проверке результата разных функций, код возврата которых сравнивают с кодами ошибок, то в приводимых далее синтетических примерах я буду использовать переменную с именем err, а code1 и code2 будут константами. При этом константы code1 и code2 не равны. Значение «other codes» будет означать любые другие константы, не равные code1 и code2.
Ошибки с использованием оператора '||'
Выражение != || !=
Синтетический пример, в котором результат условного выражения всегда будет равен истине:
if ( err != code1 || err != code2)
{
....
}
Далее представлена таблица истинности для этого примера кода:
Теперь посмотрим на реальный пример ошибки, найденной в проекте LibreOffice.
V547 Expression is always true. Probably the '&&' operator should be used here. sbxmod.cxx 1777
enum SbxDataType {
SbxEMPTY = 0,
SbxNULL = 1,
....
};
void SbModule::GetCodeCompleteDataFromParse(
CodeCompleteDataCache& aCache)
{
....
if( (pSymDef->GetType() != SbxEMPTY) || // <=
(pSymDef->GetType() != SbxNULL) ) // <=
aCache.InsertGlobalVar( pSymDef->GetName(),
pParser->aGblStrings.Find(pSymDef->GetTypeId()) );
....
}
Выражение == || !=
Синтетический пример, в котором результат всего условного выражения не зависит от результата подвыражения (err == code1):
if ( err == code1 || err != code2)
{
....
}
Далее представлена таблица истинности для этого примера кода:
Теперь посмотрим на реальный пример ошибки, найденной в проекте FreeBSD.
V590 Consider inspecting the 'error == 0 || error != — 1' expression. The expression is excessive or contains a misprint. nd6.c 2119
int
nd6_output_ifp(....)
{
....
/* Use the SEND socket */
error = send_sendso_input_hook(m, ifp, SND_OUT,
ip6len);
/* -1 == no app on SEND socket */
if (error == 0 || error != -1) // <=
return (error);
....
}
Не сильно он отличается от синтетического примера, не правда ли?
Ошибки с использованием оператора '&&'
Выражение == && ==
Синтетический пример, в котором результат условного выражения всегда будет ложным:
if ( err == code1 && err == code2)
{
....
}
Далее представлена таблица истинности для этого примера кода:
Теперь посмотрим на реальный пример ошибки, найденной в проекте SeriousEngine.
V547 Expression is always false. Probably the '||' operator should be used here. entity.cpp 3537
enum RenderType {
....
RT_BRUSH = 4,
RT_FIELDBRUSH = 8,
....
};
void
CEntity::DumpSync_t(CTStream &strm, INDEX iExtensiveSyncCheck)
{
....
if( en_pciCollisionInfo == NULL) {
strm.FPrintF_t("Collision info NULLn");
} else if (en_RenderType==RT_BRUSH && // <=
en_RenderType==RT_FIELDBRUSH) { // <=
strm.FPrintF_t("Collision info: Brush entityn");
} else {
....
}
....
}
Выражение == && !=
Синтетический пример, в котором результат всего условного выражения не зависит от результата подвыражения «err != code2»:
if ( err == code1 && err != code2)
{
....
}
Далее представлена таблица истинности для этого примера кода:
Теперь посмотрим на реальный пример ошибки, найденной в проекте ChakraCore — JavaScript-движке для Microsoft Edge.
V590 Consider inspecting the 'sub[i] != '-' && sub[i] == '/'' expression. The expression is excessive or contains a misprint. rl.cpp 1388
const char *
stristr
(
const char * str,
const char * sub
)
{
....
for (i = 0; i < len; i++)
{
if (tolower(str[i]) != tolower(sub[i]))
{
if ((str[i] != '/' && str[i] != '-') ||
(sub[i] != '-' && sub[i] == '/')) { / <=
// if the mismatch is not between '/' and '-'
break;
}
}
}
....
}
Ошибки с использованием оператора '?:'
V502 Perhaps the '?:' operator works in a different way than it was expected. The '?:' operator has a lower priority than the '|' operator. ata-serverworks.c 166
static int
ata_serverworks_chipinit(device_t dev)
{
....
pci_write_config(dev, 0x5a,
(pci_read_config(dev, 0x5a, 1) & ~0x40) |
(ctlr->chip->cfg1 == SWKS_100) ? 0x03 : 0x02, 1);
}
....
}
В заключение хочу сказать про тернарный оператор '?:'. Его приоритет почти самый низкий среди всех операторов. Ниже только у присваивания, throw и оператора «запятая». Приведённая в примере ошибка была найдена в ядре FreeBSD. Здесь тернарным оператором воспользовались для выбора нужного флажка и чтобы написать короткий красивый код. Но приоритет оператора побитового 'ИЛИ' выше, поэтому условное выражение вычисляется не в том порядке, в каком планировал программист. Эту ошибку я тоже решил описать в этой статье, т.к. она является очень распространённой среди проверенных мною проектов.
Заключение
Описанные шаблоны условных выражений могут нести большую опасность, если не проявлять повышенную внимательность при написании кода. Несмотря на небольшое количество операторов, всё условное выражение может пониматься неправильно. Код, в котором допустили такую ошибку, может выглядеть вполне логичным и будет пропущен после code-review. Подстраховать себя от таких ошибок можно путём проверки своего кода с помощью построения таблиц истинности, если есть сомнения в правильности условия, а также с помощью регулярных проверок статическими анализаторами.