Бросить (или, соответственно) аргумент функции NULL вместо того, чтобы все взорвалось? - PullRequest
6 голосов
/ 03 августа 2010

В предыдущих крупномасштабных приложениях, требующих высокой надежности и продолжительного времени работы, я всегда был для проверки аргумента функции указателя, когда он был задокументирован как "никогда не должно быть NULL".Затем я бы выдал исключение std::invalid_argument или подобное, если бы аргумент на самом деле был равен NULL в C ++ и возвратил бы код ошибки в C.

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

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

Есть какие-нибудь мысли или лучшие практики по этому поводу?

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

Ответы [ 11 ]

10 голосов
/ 03 августа 2010

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

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

5 голосов
/ 03 августа 2010

Определенно генерируйте исключение C ++ - его можно зарегистрировать и диагностировать намного проще, чем ваша программа позже.

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

3 голосов
/ 03 августа 2010

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

Если ваша функция очень короткая и используется в замкнутых циклах, проверка на NULL может привести к значительной потере производительности. Один очень реальный пример - стандартная функция C mbrtowc, используемая для декодирования многобайтовых символов. Если вашему приложению необходимо выполнять побайтовое или посимвольное декодирование, выделенный декодер UTF-8 может быть на 20% быстрее, чем оптимальная реализация UTF-8 mbrtowc, просто из-за того, что требуется последнее в начале делать кучу бесполезных проверок, в основном проверок NULL-указателей, и он вызывается много раз для очень маленьких данных.

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

Если вы не пишете код для встроенных систем, NULL является наименее опасным недопустимым указателем на разыменование, поскольку он немедленно приведет к исключению на уровне операционной системы (сигнал / и т. Д.). Если вы не хотите на это полагаться, почему бы просто не использовать assert(ptr);, чтобы вы могли легко включать / отключать проверки для отладки?

2 голосов
/ 03 августа 2010

Итак, настоящий вопрос: При написании библиотеки C ++ / C, какие возможные проблемы следует учитывать при указании стратегии обработки исключений / ошибок в конкретном случае выбора выбрасывания исключения (библиотека C ++) или возврата кода ошибки (библиотека C) вместо того, чтобы позволить всему этому взорваться когда аргументы функции не соответствуют задокументированным ограничениям?

Дизайн по контракту кто-нибудь? Я думаю, что это то, что Нил Баттерворт описывает частично. Если спецификация для функции говорит, что если значения аргумента находятся в пределах указанных ограничений (предусловий), то выходные данные будут в пределах указанных ограничений (постусловий), и не будет никакого нарушения каких-либо указанных инвариантов. Если пользователь вашей библиотечной функции нарушает задокументированные предварительные условия, тогда это сезон открытых дверей для «неопределенного поведения», столь любимого людьми из C ++. Это тоже должно быть задокументировано. Вы можете указать, что функция будет перехватывать и обрабатывать «недопустимые» значения аргументов (исключения C ++ / коды ошибок), но тогда возникает вопрос, являются ли они по-прежнему недопустимыми аргументами, поскольку ответ функции на это указанное предварительное условие затем должен стать указанным постусловием.

Это все связано с опубликованным кодом / двоичными файлами.

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

2 голосов
/ 03 августа 2010

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

Если вы когда-нибудь задумывались об этом, вам следует вместо этого использовать assert().Таким образом, приложение гарантированно корректно завершит работу и сгенерирует дамп ядра.

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

Для подобных случаев я лично предпочитаю использовать assert(), сопровождаемый исчерпывающим модульным тестом.Обычные высокоуровневые тесты редко могут в достаточной степени охватить код.Для релизных сборок я отключаю assert() s (используя -DNDEBUG), как в производстве, в таких редких случаях клиенты обычно предпочитают модуль, который может работать с недостатком, но работает как-то и не падает.(Печальная реальность коммерческого программного обеспечения.)

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

Подводя итог.Если NULL действительно не может произойти, используйте assert().Если у вас уже есть обработчик исключений, обработайте NULL как простую внутреннюю ошибку.

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

2 голосов
/ 03 августа 2010

Если первое, что вы делаете, это проверяете, что параметр не равен NULL, то вы должны использовать ссылку.

void Foo(Bar *const pBar)
{
    if (pBar == NULL) { throw error(); }

    // Now do something with pBar
}

Измените это на:

void Foo(Bar &bar)
{
    // Now use bar, safe in the knowledge it's not NULL.
}

Имейте в виду, что вы все равно можете получить нулевую ссылку:

Bar *const pBar = NULL;
Foo(*pBar); // Will crash *inside* of Foo, rather than at the point of dereference.

Это означает, что вам необходимо продолжить проверку параметров.

2 голосов
/ 03 августа 2010

Если параметр функции является указателем, который не должен быть НЕДЕЙСТВИТЕЛЕН, то не следует ли изменить параметр на ссылку?Это, вероятно, не изменит отчет о том, когда вы получаете приведение из NULL-указателя, но даст статическим контролерам типов больше шансов обнаружить ошибку при компиляции.

1 голос
/ 03 августа 2010

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

Что лучше, когда они отлаживают свой код:

  • Разыменование нулевого указателя с трассировкой стека на 3 или более уровнях глубококод вашей библиотеки.
  • Исключение только из одного уровня в библиотеке, которое говорит: "Вы сделали это неправильно".

Я думаю, что ответ довольно прост, особенно если учестьотчеты об ошибках в первом случае, которые будут пустой тратой вашего времени.

1 голос
/ 03 августа 2010

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

Риск того, что исключение может где-то быть пойманным, требует, чтобы вы не сгенерировать исключение и просто дать ему умереть.

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

1 голос
/ 03 августа 2010

Я также пришел к выводу, что лучше дать сбой приложению при обращении к гнилой указке, чем проверять параметры всех функций. На уровне интерфейса в общедоступной области, на уровне API, где приложения вызывают библиотеки, должны быть проверки и надлежащая обработка ошибок (в конечном счете, с диагностикой), но оттуда не требуется никаких избыточных и проверок. Посмотрите на стандартную библиотеку lib, она использует именно эту философию: если вы позвоните strlen или memset или qsort с NULL, вы получите вполне заслуженный сбой. Это имеет смысл, так как в 99,9% случаев вы будете указывать хорошие указатели, а проверка в этой функции будет избыточной.

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...