Ловить assert() с побочными эффектами

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

assert(function_that_should_always_be_called());

Мы уже используем собственную реализацию assert(), но оценка выражения с помощью NDEBUG приведет к неприемлемым ухудшениям производительности. Есть ли расширение GCC или флаг, который мы можем передать, который будет запускать предупреждения об ошибках компиляции/ошибки? При достаточно простом потоке управления GCC должен иметь возможность определить, что вы вызываете только чистые функции.

Ответ 1

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

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

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

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

Вот как вы можете использовать оптимизатор своего компилятора для проведения такого статического анализа. Предположим, что вы решили заменить определение макроса assert на:

extern int not_supposed_to_survive;
#define assert(expr) ((void)(not_supposed_to_survive || (expr)))

Если expr имеет какой-либо побочный эффект, выполнение эффекта зависит от значения глобальной переменной not_supposed_to_survive. Но если expr не имеет никакого побочного эффекта, значение глобальной переменной не имеет значения (обратите внимание, что результат expr отбрасывается). Хороший оптимизатор знает это и устранит нагрузку глобальной переменной not_supposed_to_survive, следовательно, имя переменной.

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

например. с gcc 4.8:

int g;

int foo() { return ++g; }

int main() {
    assert(foo());
    return 0;
}

gcc -O2 assert_effect.c
/tmp/ccunynya.o: In function `main':
assert_effect.c:(.text.startup+0x2): undefined reference to `not_supposed_to_survive'
collect2: error: ld returned 1 exit status

Компилятор помог мне найти сомнительное утверждение! С другой стороны, если я заменил ++g на g+1, ошибка ссылки исчезнет, ​​и мне не нужно ее расследовать. В самом деле, это утверждение гарантировано безвредно.

Конечно, концепция доказуемого побочного эффекта ограничена тем, что оптимизатор "может видеть". Для более точного анализа я бы рекомендовал использовать оптимизацию времени соединения (gcc -flto) для анализа блоков компиляции.

Обновление: Я применил это на реальной базе кода С++, используя gcc 5.3. Чтобы использовать оптимизацию времени соединения, вы по существу используете gcc -flto -g в качестве компилятора/компоновщика (параметр -g в компиляторе/компоновщике для получения ссылки на ссылки на ошибки ссылок) и gcc-ar и gcc-ranlib в качестве архиватора/индексатора для любых статических библиотек.

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

  • Виртуальные вызовы функций
  • Нетривиальные петли/рекурсии (где оптимизатор не может доказать, что они конечны)

Кроме того, я бы также получил некоторые утверждения, которые действительно содержали побочные эффекты, но они безвредны или несущественны, например:

  • Функции, содержащие записи журнала
  • Функции, которые кэшируют их результат (ы)

Ответ 2

Даже если GCC может надежно обнаружить чистые вычисления (что потребует решения проблемы остановки), флаг должен будет обладать дополнительными магическими полномочиями, чтобы заметить, что нечистое вычисление было передано в качестве аргумента для вашего домашнего макроса assert, Расширение не могло помочь - что именно он должен делать?

Решение вашей проблемы -

  • Нанять компетентных разработчиков.
  • Просветите своих разработчиков о том, как использовать утверждения (между прочим).
  • Прочитайте обзоры кода.
  • Выполняйте все тесты против поставляемых версий - если утверждения недоступны в результатах, то assert (function_that_should_always_be_called()) ничем не отличается от простого исключения функции_that_should_always_be_called(), что является вопиющей ошибкой, которая должна быть обнаружена при тестировании.

Ответ 3

С достаточно простым потоком управления GCC должен быть способен определить, что вы вызываете только чистые функции.

И если это не достаточно простой поток управления, как он узнает, чисто он или нет?


Что-то вроде этого, скорее всего, ваш лучший выбор:

#ifdef NDEBUG
#define assert(s) do { (s); } while(false)
#else
// ...
#endif

Выделится несколько выражений, включая функции с __attribute__((pure)).

Наиболее логичным решением было бы просто просмотреть ваш код и исправить ошибки.

Ответ 4

Я не уверен, достаточно ли этого для приложения, которое вы описали, но cppcheck ищет "assertWithSideEffect" s: http://cppcheck.sourceforge.net/devinfo/doxyoutput/checkassert_8cpp_source.html

Вот как выглядит сообщение компиляции: [assertWithSideEffect] myFile.cpp: 42: warning: Нечистая функция: 'myFunction' вызывается внутри инструкции assert. Операторы утверждения удаляются из сборников релизов, поэтому код внутри инструкции assert не выполняется. Если код необходим и в сборках релизов, это ошибка.

"Cppcheck - это инструмент статического анализа для кода C/С++. В отличие от компиляторов C/С++ и многих других инструментов анализа он не обнаруживает синтаксические ошибки в коде. Cppcheck в первую очередь определяет типы ошибок, которые компиляторы обычно не обнаруживают. Цель состоит в том, чтобы обнаруживать только реальные ошибки в коде (т.е. Иметь нулевые ложные срабатывания)". http://cppcheck.sourceforge.net/