Пометить переменную как не NULL после BOOST_REQUIRE в PVS-Studio

Я использую PVS-Studio для анализа своего тестового кода. Часто встречаются конструкции вида

const noAnimal* animal = dynamic_cast<noAnimal*>(...);
BOOST_REQUIRE(animal);
BOOST_REQUIRE_EQUAL(animal->GetSpecies(), ...);

Однако я все еще получаю предупреждение V522 There might be dereferencing of a potential null pointer 'animal' для последней строки.

Я знаю, что можно пометить функции как "не возвращающие NULL", но можно ли также пометить функцию как валидную проверку NULL или сделать так, чтобы PVS-Studio каким-то другим образом знал, что animal не может быть NULL после BOOST_REQUIRE(animal);?

Это также происходит, если указатель сначала проверяется через любой вариант assert.


person Flamefire    schedule 14.11.2017    source источник


Ответы (2)


Спасибо за интересный пример. Подумаем, что можно сделать с макросом BOOST_REQUIRE.

На данный момент могу посоветовать вам следующее решение:

Где-то после

#include <boost/test/included/unit_test.hpp>

ты можешь написать:

#ifdef PVS_STUDIO
  #undef BOOST_REQUIRE
  #define BOOST_REQUIRE(expr) do { if (!(expr)) throw "PVS-Studio"; } while (0)
#endif

Таким образом, вы подскажете анализатору, что ложное условие вызывает прерывание потока управления. Это не самое красивое решение, но, думаю, стоило рассказать о нем.

person AndreyKarpov    schedule 15.11.2017
comment
Спасибо за ответ. Хотя это возможно, было бы сложно включить это определение во все файлы тестовых сценариев. Кроме того, это не ограничивается только BOOST_REQUIRE, но также относится к assert, SDL_Assert или любому другому пользовательскому макросу, который может использовать пользователь. Итак, я вижу 2 решения: либо позволить пользователю указать список имен макросов assert, после чего условие считается истинным (поскольку это причина для assert). - person Flamefire; 15.11.2017
comment
Или, по крайней мере, иметь глобальный файл только для PVS с определениями, которые читаются PVS и предполагаются используемыми (аналогично cpp.hint). Таким образом, вы можете поместить такие определения в центральное место. Однако я думаю, что это будет сложно, так как определения могут быть сделаны в любом включенном файле. - person Flamefire; 15.11.2017

Отвечать на комментарий большим — плохая идея, поэтому вот мой подробный ответ по следующей теме:

Хотя это возможно, было бы сложно включить это определение во все файлы тестовых сценариев. Также это не ограничивается только BOOST_REQUIRE, но также применимо к assert, SDL_Assert или любому другому пользовательскому макросу, который может использовать пользователь.

Следует понимать, что существует три типа тестовых макросов и о каждом стоит поговорить отдельно.

Макросы первого типа просто предупреждают о том, что в отладочной версии что-то пошло не так. Типичный пример — макрос assert. Следующий код заставит анализатор PVS-Studio выдать предупреждение:

T* p = dynamic_cast<T *>(x);
assert(p);
p->foo();

Анализатор укажет на возможное разыменование нулевого указателя и будет прав. Проверки, использующей assert, недостаточно, поскольку она будет удалена из версии Release. То есть получается, что чека нет. Лучший способ реализовать это — переписать код примерно так:

T* p = dynamic_cast<T *>(x);
if (p == nullptr)
{
  assert(false);
  throw Error;
}
p->foo();

Этот код не вызовет предупреждение.

Вы можете возразить, что на 100 % уверены, что dynamic_cast никогда не вернет nullptr. Я не принимаю этот аргумент. Если вы полностью уверены, что приведение ВСЕГДА правильное, вам следует использовать более быстрый static_cast. Если вы не уверены, вы должны проверить указатель, прежде чем разыменовывать его.

Ну, хорошо, я понимаю твою точку зрения. Вы уверены, что с кодом все в порядке, но на всякий случай нужно проверить с помощью dynamic_cast. ОК, тогда используйте следующий код:

assert(dynamic_cast<T *>(x) != nullptr);
T* p = static_cast<T *>(x);
p->foo();

Мне это не нравится, но по крайней мере так быстрее, так как более медленный оператор dynamic_cast в релизной версии будет опущен, а анализатор промолчит.

Переходим к следующему типу макросов.

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

Основная проблема с этими макросами заключается в том, что функции не помечены как невозвратные. Вот пример.

Предположим, у нас есть функция, которая генерирует сообщение об ошибке, вызывая исключение. Вот как выглядит его объявление:

void Error(const char *message);

А вот как объявляется тестовый макрос:

#define ENSURE(x) do { if (!x) Error("zzzz"); } while (0)

Используя указатель:

T* p = dynamic_cast<T *>(x);
ENSURE(p);
p->foo();

Анализатор выдаст предупреждение о возможном разыменовании нулевого указателя, но на самом деле код безопасен. Если указатель нулевой, функция Error вызовет исключение и, таким образом, предотвратит разыменование указателя.

Нам просто нужно сообщить об этом анализатору с помощью одного из средств аннотации функции, например:

[[noreturn]] void Error(const char *message);

or:

__declspec(noreturn) void Error(const char *message);

Это поможет исключить ложное предупреждение. Итак, как видите, в большинстве случаев довольно легко исправить ситуацию, используя собственные макросы.

Однако это может быть сложнее, если вы имеете дело с небрежно реализованными макросами из сторонних библиотек.

Это приводит нас к третьему типу макросов. Вы не можете их изменить, и анализатор не может понять, как именно они работают. Это обычная ситуация, так как макросы могут быть реализованы весьма экзотическими способами.

В этом случае у вас остается три варианта:

  1. подавить предупреждение с помощью одного из способов подавления ложных срабатываний, описанных в документации;
  2. используйте технику, описанную в предыдущем ответе;
  3. Свяжитесь с нами по электронной почте.

Мы постепенно добавляем поддержку различных хитрых макросов из популярных библиотек. На самом деле анализатор уже знаком с большинством конкретных макросов, с которыми вы можете столкнуться, но фантазия программистов неиссякаема, и мы просто не можем предусмотреть каждую возможную реализацию.

person AndreyKarpov    schedule 16.11.2017
comment
Я не согласен с вами по поводу истории dynamic_cast. Во-первых, иногда вам нужен dynamic_cast (множественное наследование...), но вы уверены, что он не может потерпеть неудачу (например, виртуальная функция возвращает тип). Или, во-вторых, вы утверждаете, что он действителен, но по-прежнему сохраняете dynamic_cast, поэтому вы получаете исключение Null-Pointer-Exception в режиме выпуска вместо UD. Это ИМО лучше, чем дополнительная проверка и бросок для (должного) невозможного случая. (например, функция вернула тип, поэтому она должна быть приводимой, но вы хотите защитить от ошибки C&P в этой функции. Хитрые макросы: поэтому я предложил вариант конфигурации - person Flamefire; 16.11.2017