В программировании логическое выражение — это языковая конструкция, которая оценивается как истинная или ложная. Во многих книгах, обучающих программированию «с нуля», обсуждаются возможные операции над логическими выражениями, знакомыми каждому новичку. В этой статье я не буду говорить о том, что оператор И имеет более высокий приоритет, чем ИЛИ. Вместо этого я расскажу о распространенных ошибках, которые допускают программисты в простых условных выражениях, состоящих не более чем из трех операторов, и покажу, как можно проверить свой код с помощью таблиц истинности. Описанные здесь ошибки допущены разработчиками таких известных проектов, как FreeBSD, Microsoft ChakraCore, Mozilla Thunderbird, LibreOffice и многих других.

Введение

Разрабатываю статический анализатор кода C/C++/C#, известный как PVS-Studio. Моя работа связана с работой как с открытым исходным кодом, так и с проприетарным кодом различных проектов, и в результате этой деятельности я пишу много статей по анализу открытых проектов, где рассказываю о найденных ошибках и дефектах. в этих проектах. При всем том огромном количестве кода, прошедшего через наш анализатор, мы начали замечать определенные закономерности ошибок программирования. Например, мой коллега Андрей Карпов однажды написал статью об эффекте последней строки после того, как собрал большую коллекцию примеров ошибок, допущенных в последних строках или блоках похожих на вид фрагментов кода.

В начале этого года я использовал анализатор для проверки некоторых проектов крупных ИТ-компаний, которые, следуя современной тенденции, выкладывают исходники своих проектов в открытый доступ по бесплатным лицензиям. Я начал замечать, что почти в каждом проекте есть ошибки в условных выражениях, связанные с неправильным использованием условных операторов. Сами выражения довольно просты и состоят всего из трех операторов:

  • != || !=
  • == || !=
  • == && ==
  • == && !=

Всего с помощью этих операторов можно написать 6 условных выражений, но 4 из них неверны: два всегда истинны или ложны; в двух других результат всего выражения не зависит от результата одного из его подвыражений.

Чтобы доказать, что условное выражение неверно, я построю таблицу истинности для каждого примера. Я также приведу пример из реального кода для каждого случая. Речь пойдет и о тернарном операторе ?:, приоритет которого чуть ли не самый низкий, хотя мало кто из программистов о нем знает.

Так как некорректные условные выражения чаще всего встречаются в коде, который проверяет возвращаемые значения различных функций, сравнивая их с кодами ошибок, ниже в синтетических примерах я буду использовать переменную err, а code1 и code2 будем использовать как константы, которые не равны. Значение «другие коды» будет обозначать любые другие константы, не равные code1 и code2.

Неправильное использование || оператор

Выражение != || знак равно

Ниже приведен синтетический пример, в котором условное выражение всегда будет иметь значение true:

if ( err != code1 || err != code2)
{
  ....
}

Это таблица истинности для этого кода:

А вот реальный пример этой ошибки из проекта LibreOffice.

Выражение V547 всегда истинно. Вероятно, здесь следует использовать оператор &&. 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 Рассмотрите возможность проверки «ошибка == 0 || ошибка != — 1’ выражение. Выражение является избыточным или содержит опечатку. 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 всегда ложно. Вероятно, здесь следует использовать оператор ||. сущность.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 Рассмотрите возможность проверки выражения ‘sub[i] != ‘-’ && sub[i] == ‘/’’. Выражение является избыточным или содержит опечатку. 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 Возможно, оператор ?: работает не так, как ожидалось. Оператор ‘?:’ имеет более низкий приоритет, чем оператор ‘|’. 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. Авторы использовали тернарный оператор для выбора нужного флажка и ради короткого аккуратного кода. Однако побитовое И имеет более высокий приоритет, поэтому условное выражение оценивается в неправильном порядке. Я решил включить эту ошибку в статью, потому что она очень часто встречается в отсканированных мною проектах.

Вывод

Паттерны ошибок в условных выражениях, описанные в этой статье, могут быть очень опасны, если вы недостаточно внимательно относитесь к написанию кода. Несмотря на небольшое количество операторов, некорректное условное выражение в целом может быть неверно истолковано компилятором. Такой код может выглядеть вполне осмысленно и пройти код-ревью. Чтобы обезопасить себя от этих ошибок, используйте таблицы истинности для проверки ваших выражений, когда вы сомневаетесь, а также убедитесь, что вы регулярно сканируете свой код статическими анализаторами.