Топ 10 ошибок в проектах C++ за 2019 год
Ещё один год стремится к окончанию, поэтому настало время заварить себе кофе и перечитать обзоры ошибок за прошедший год. Конечно, на это уйдёт много времени, поэтому эта статья и была написана. Предлагаю взглянуть на наиболее интересные темные места проектов, которые встретились нам в 2019 году в проектах, написанных на C и C++.
Десятое место: «Какая у нас ОС?»
V1040 Possible typo in the spelling of a pre-defined macro name. The '__MINGW32_' macro is similar to '__MINGW32__'. winapi.h 4112
#if !defined(__UNICODE_STRING_DEFINED) && defined(__MINGW32_)
#define __UNICODE_STRING_DEFINED
#endif
Здесь была допущена опечатка в имени макроса __MINGW32_ (MINGW32 объявляет __MINGW32__). В других местах проекта как раз проверка написана правильно:
Это, кстати, была не только первая ошибка в статье «CMake: тот случай, когда проекту непростительно качество его кода», но и вообще первая реальная ошибка, найденная диагностикой V1040 в реальном открытом проекте (19 августа 2019).
Девятое место: «Кто первый?»
V502 Perhaps the '?:' operator works in a different way than it was expected. The '?:' operator has a lower priority than the '==' operator. mir_parser.cpp 884
enum Opcode : uint8 {
kOpUndef,
....
OP_intrinsiccall,
OP_intrinsiccallassigned,
....
kOpLast,
};
bool MIRParser::ParseStmtIntrinsiccall(StmtNodePtr &stmt, bool isAssigned) {
Opcode o = !isAssigned ? (....)
: (....);
auto *intrnCallNode = mod.CurFuncCodeMemPool()->New(....);
lexer.NextToken();
if (o == !isAssigned ? OP_intrinsiccall : OP_intrinsiccallassigned) {
intrnCallNode->SetIntrinsic(GetIntrinsicID(lexer.GetTokenKind()));
} else {
intrnCallNode->SetIntrinsic(static_cast(....));
}
....
}
Нам интересна следующая часть этого кода:
if (o == !isAssigned ? OP_intrinsiccall : OP_intrinsiccallassigned) {
....
}
Оператор '==' имеет более высокий приоритет, чем тернарный оператор (?:). Из-за этого условное выражение вычисляется неправильно. Написанный код эквивалентен следующему:
if ((o == !isAssigned) ? OP_intrinsiccall : OP_intrinsiccallassigned) {
....
}
А с учётом того, что константы OP_intrinsiccall и OP_intrinsiccallassigned имеют ненулевые значения, то это условие всегда возвращает истинное значение. Тело ветки else является недостижимым кодом.
Эта ошибка пришла в наш топ из статьи «Проверка кода компилятора Ark Compiler, недавно открытого компанией Huawei».
Восьмое место: «Опасность побитовых операций»
V1046 Unsafe usage of the bool' and 'int' types together in the operation '&='. GSLMultiRootFinder.h 175
int AddFunction(const ROOT::Math::IMultiGenFunction & func) {
ROOT::Math::IMultiGenFunction * f = func.Clone();
if (!f) return 0;
fFunctions.push_back(f);
return fFunctions.size();
}
template
bool SetFunctionList( FuncIterator begin, FuncIterator end) {
bool ret = true;
for (FuncIterator itr = begin; itr != end; ++itr) {
const ROOT::Math::IMultiGenFunction * f = *itr;
ret &= AddFunction(*f);
}
return ret;
}
Исходя из кода ожидалось, что функция SetFunctionList обходит список итераторов. И если хоть один из них будет невалидным, то возвращаемое значение будет false, иначе true.
Однако в реальности функция SetFunctionList может возвращать значение false даже для валидных итераторов. Разберёмся в ситуации.Функция AddFunction возвращает количество валидных итераторов в списке fFunctions. Т.е. при добавлении ненулевых итераторов, размер этого списка будет последовательно увеличиваться: 1, 2, 3, 4 и т.д. Вот тут и начнёт проявлять себя ошибка в коде:
ret &= AddFunction(*f);
Т.к. функция возвращает результат типа int, а не bool, то операция '&=' с чётными числами будет давать значение false. Ведь младший бит чётных чисел всегда будет равен нулю. Следовательно, такая неочевидная ошибка будет портить результат функции SetFunctionsList даже для валидных аргументов.
Если вы внимательно читали код из примера (а вы же внимательно читали, правда?), то вы могли обратить внимание, что это код из проекта ROOT. Мы его, конечно, проверяли: «Анализ кода ROOT — фреймворка для анализа данных научных исследований».
Седьмое место: «Путаница в переменных»
V1001 [CWE-563] The 'Mode' variable is assigned but is not used by the end of the function. SIModeRegister.cpp 48
struct Status {
unsigned Mask;
unsigned Mode;
Status() : Mask(0), Mode(0){};
Status(unsigned Mask, unsigned Mode) : Mask(Mask), Mode(Mode) {
Mode &= Mask;
};
....
};
Очень опасно давать аргументам функций те же самые имена, что и членам класса. Очень легко запутаться. Перед нами как раз такой случай. Это выражение не имеет смысла:
Mode &= Mask;
Меняется аргумент функции. И всё. Этот аргумент больше никак не используется. Скорее всего, надо было написать так:
Status(unsigned Mask, unsigned Mode) : Mask(Mask), Mode(Mode) {
this->Mode &= Mask;
};
А это ошибка из LLVM. У нас есть традиция время от времени анализировать этот проект. В этом году у нас также появилась статья о проверке.
Шестое место: «В C++ свои законы»
Следующая ошибка появляется в коде из-за того, что правила С++ не всегда совпадают с математическими правилами или «здравым смыслом». Заметите сами, где в небольшом отрывке кода содержится ошибка?
V709 Suspicious comparison found: 'f0 == f1 == m_fractureBodies.size ()'. Remember that 'a == b == c' is not equal to 'a == b && b == c'. btFractureDynamicsWorld.cpp 483
btAlignedObjectArray m_fractureBodies;
void btFractureDynamicsWorld::fractureCallback()
{
for (int i = 0; i < numManifolds; i++)
{
....
int f0 = m_fractureBodies.findLinearSearch(....);
int f1 = m_fractureBodies.findLinearSearch(....);
if (f0 == f1 == m_fractureBodies.size())
continue;
....
}
....
}
Казалось бы, условие проверяет, что f0 равно f1 и равно количеству элементов в m_fractureBodies. Похоже, что это сравнение должно было проверить, находятся ли f0 и f1 в конце массива m_fractureBodies, поскольку они содержат найденную методом findLinearSearch () позицию объекта. Однако на самом деле это выражение превращается в проверку, равны ли f0 и f1, а затем в проверку, равно ли m_fractureBodies.size () результату f0 == f1. В итоге, третий операнд здесь сравнивается с 0 или 1.
Красивая ошибка! И, к счастью, достаточно редкая. Пока мы встречали её только в трёх открытых проектах, и, что интересно, все они были как раз игровыми движками. Кстати, это не единственная ошибка, которую мы обнаружили в Bullet. Самые интересные попали в нашу статью «PVS-Studio заглянул в движок Red Dead Redemption — Bullet».
Пятое место: «Что есть конец строки?»
Следующая ошибка легко обнаруживается, если знать об одной тонкости.
V739 EOF should not be compared with a value of the 'char' type. The 'ch' should be of the 'int' type. json.cpp 762
void JsonIn::skip_separator()
{
signed char ch;
....
if (ch == ',') {
if( ate_separator ) {
....
}
....
} else if (ch == EOF) {
....
}
Это одна из тех ошибок, которые бывает сложно заметить, если не знать, что EOF определен как -1. Соответственно, если пытаться сравнивать его с переменной типа signed char, условие почти всегда оказывается false. Единственное исключение, это если кодом символа будет 0xFF (255). При сравнении такой символ превратится в -1 и условие окажется верным.
В этом топе очень много ошибок, связанных с играми: от движков до открытых игр. Как вы уже могли догадаться, это место также пришло к нам из этой сферы. Больше ошибок вы можете посмотреть в статье «Cataclysm Dark Days Ahead, статический анализ и рогалики».
Четвертое место: «Магия числа Пи»
V624 There is probably a misprint in '3.141592538' constant. Consider using the M_PI constant from. PhysicsClientC_API.cpp 4109
B3_SHARED_API void b3ComputeProjectionMatrixFOV(float fov, ....)
{
float yScale = 1.0 / tan((3.141592538 / 180.0) * fov / 2);
....
}
Небольшая опечатка в числе Пи (3,141592653…), пропущено число »6» на 7-ой позиции в дробной части.
Возможно, ошибка в десятимиллионной доле после запятой и не приведет к ощутимым последствиям, но все-таки стоит пользоваться уже существующими библиотечными константами без опечаток. Для числа Пи, например, есть константа M_PI из заголовка math.h.
Эта ошибка из уже знакомой нам по шестому месту статьи «PVS-Studio заглянул в движок Red Dead Redemption — Bullet». Если вы ещё не отложили её на потом, то это последний шанс.
Небольшое лирическое отступление
Вот мы и приблизились к тройке самых интересных ошибок. Как вы могли заметить, они отсортированы не по катастрофичности последствий их наличия, а по сложности обнаружения. Ведь в конечном счёте самое главное преимущество статического анализа над code review в том, что машина не устаёт и ничего не забывает. :)
А теперь предлагаю вашему вниманию первую тройку.
Третье место: «Неуловимое исключение»
V702 Classes should always be derived from std: exception (and alike) as 'public' (no keyword was specified, so compiler defaults it to 'private'). CalcManager CalcException.h 4
class CalcException : std::exception
{
public:
CalcException(HRESULT hr)
{
m_hr = hr;
}
HRESULT GetException()
{
return m_hr;
}
private:
HRESULT m_hr;
};
Анализатор обнаружил класс, унаследованный от класса std: exception через модификатор private (модификатор по умолчанию, если ничего не указано). Проблема такого кода заключается в том, что при попытке поймать общее исключение std: exception исключение типа CalcException будет пропущено. Такое поведение возникает потому, что приватное наследование исключает неявное преобразование типов.
Да, не хотелось бы увидеть падение программы из-за упущенного модификатора public. Кстати, уверен, что вы точно хоть раз использовали в своей жизни приложение, исходный код которого мы сейчас смотрели. Это старый добрый стандартный калькулятор Windows, который мы также проверяли.
Второе место: «Незакрытые HTML-теги»
V735 Possibly an incorrect HTML. The »
static QString makeAlgebraLogBaseConversionPage() {
return
BEGIN
INDEX_LINK
TITLE(Book::tr("Logarithmic Base Conversion"))
FORMULA(y = log(x) / log(a), logax = log(x) / log(a))
END;
}
Как это часто бывает с C/C++ кодом — из исходника ничего не понятно, поэтому обратимся к препроцессированному коду для этого фрагмента:
Анализатор обнаружил незакрытый тег Удивлены, что мы умеем проверять и такое? Когда я впервые это увидел, то был впечатлён. Так что мы немножечко анализируем html-код. Правда, только в коде C++. :) Это не только второе место, но и второй калькулятор в нашем топе. Со списком всех ошибок вы можете ознакомится в статье «По следам калькуляторов: SpeedCrunch». Попробуйте обнаружить её сами: V560 A part of conditional expression is always true: ('\n' != c). params.c 136. Странно, не так ли? Давайте посмотрим на кое-что любопытное в этом же проекте, но в другом файле (charset.h): Конечно, вы можете сказать, что этот макрос похож на #define true false, и такой код никогда не пройдёт code review. Однако этот код прошёл ревью и благополучно ждал нас в репозитории проекта. Если вам нужен более подробный разбор ошибки, то он есть в статье «Для тех, кто хочет поиграть в детектива: найди ошибку в функции из Midnight Commander». За прошедший год мы нашли много ошибок. Это были привычные ошибки copy-paste, ошибки в константах, незакрытые теги и множество других проблем. Но наш анализатор совершенствуется и учится искать всё больше и больше багов, поэтому это далеко не конец, и новые статьи о проверке проектов будут выходить так же часто, как и раньше. Если кто-то читает наши статьи впервые, то на всякий случай уточню, что все эти ошибки были найдены с помощью статического анализатора кода PVS-Studio, который мы предлагаем скачать и попробовать. Анализатор умеет выявлять ошибки в коде программ, написанных на языках: C, C++, C# и Java. Вот мы и дошли до конца! Если же вы пропустили первые два уровня, то предлагаю не упускать возможность и пройти их вместе с нами: C# и Java. Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Maxim Zvyagintsev. Top 10 Bugs Found in C++ Projects in 2019.Первое место: «Неуловимые стандартные функции»
Вот мы и добрались до первого места. Впечатляюще-странная проблема, которая прошла code review.static int
EatWhitespace (FILE * InFile)
/* ----------------------------------------------------------------------- **
* Scan past whitespace (see ctype(3C)) and return the first non-whitespace
* character, or newline, or EOF.
*
* Input: InFile - Input source.
*
* Output: The next non-whitespace character in the input stream.
*
* Notes: Because the config files use a line-oriented grammar, we
* explicitly exclude the newline character from the list of
* whitespace characters.
* - Note that both EOF (-1) and the nul character ('\0') are
* considered end-of-file markers.
*
* ----------------------------------------------------------------------- **
*/
{
int c;
for (c = getc (InFile); isspace (c) && ('\n' != c); c = getc (InFile))
;
return (c);
} /* EatWhitespace */
Теперь давайте посмотрим, на что ругается анализатор: #ifdef isspace
#undef isspace
#endif
....
#define isspace(c) ((c)==' ' || (c) == '\t')
Так, а это уже действительно странно… Выходит, если переменная c равняется '\n', то совершенно безобидная на первый взгляд функция isspace© вернёт ложь и вторая часть этой проверки не будет выполнена из-за short-circuit evaluation. Если же isspace© будет выполнена, то переменная c равна или ' ' или '\t', а это явно не равно '\n'.Заключение