Логічні вирази в C/C++. Як помиляються професіонали

Логічне вираження в програмуванні — конструкція мови програмування, результат обчислення якої є «істина» або «брехня». У багатьох книгах по програмуванню, призначених для вивчення мови «з нуля», наводяться можливі операції над логічними виразами, з якими стикався кожен початківець розробник. У цій статті я не буду розповідати, що оператор 'І' пріоритетніше оператора 'АБО'. Я розповім про поширені помилки в простих умовних виразах, що складаються всього з трьох операторів, і покажу, як можна перевірити свій код за допомогою побудови таблиць істинності. Описані помилки роблять розробники таких відомих проектів, як 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 NULL\n");
} else if (en_RenderType==RT_BRUSH && // <=
en_RenderType==RT_FIELDBRUSH) { // <=
strm.FPrintF_t("Collision info: Brush entity\n");
} 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 than the priority '|' 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. Підстрахувати себе від таких помилок можна шляхом перевірки свого коду за допомогою побудови таблиць істинності, якщо є сумніви в правильності умови, а також за допомогою регулярних перевірок статичними аналізаторами.


Якщо хочете поділитися цією статтею з англомовної аудиторією, то прошу використовувати посилання на переклад: Svyatoslav Razmyslov. Logical Expressions in C/C++. Mistakes Made by Professionals .

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.