Бросок (или соответственно) на ПУСТОМ аргументе функции по сравнению с разрешением всему этому аварийно завершиться?

В предыдущих крупномасштабных приложениях, требующих высокой устойчивости и долгие времена работы, я всегда был для проверки аргумента функции указателя, когда это было зарегистрировано, поскольку "никогда не должно быть ПУСТЫМ". Я затем бросил бы std::invalid_argument исключение, или подобный, если аргумент на самом деле был НУЛЕВЫМ в C++ и возвращает код ошибки в C.

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

Одна проблема с не проверкой ПУСТОЙ УКАЗАТЕЛЬ и разрешением аварийному завершению приложения, то, что, если указатель на самом деле не разыменовывается в той функции, а скорее сохранил для более позднего использования, то взрыв разыменования будет вне контекста и намного тяжелее диагностировать.

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

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

6
задан Johann Gerell 3 August 2010 в 09:25
поделиться

11 ответов

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

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

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

1
ответ дан 8 December 2019 в 03:08
поделиться

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

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

5
ответ дан 8 December 2019 в 03:08
поделиться

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

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

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

1
ответ дан 8 December 2019 в 03:08
поделиться

Если первое, что вы делаете, это проверяете, что параметр не 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.
}

Однако помните, что вы все равно можете получить NULL-ссылку:

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

А это значит, что вам нужно разместить проверку параметров дальше.

2
ответ дан 8 December 2019 в 03:08
поделиться

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

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

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

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

3
ответ дан 8 December 2019 в 03:08
поделиться

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

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

10
ответ дан 8 December 2019 в 03:08
поделиться

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

2
ответ дан 8 December 2019 в 03:08
поделиться

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

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

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

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

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

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

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

2
ответ дан 8 December 2019 в 03:08
поделиться

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

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

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

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

2
ответ дан 8 December 2019 в 03:08
поделиться

Если ваше приложение ведет журнал, вы можете просто записать в файл журнала что-то вроде: functionX (): got NULL pointer с отметками времени или любым другим соответствующим контекстом. Что касается решения, позволить ли ему взорваться или выполнить проверку и выдать ошибку ... Ну, это действительно зависит от того, что делает ваше приложение, каковы ваши приоритеты и так далее.Я бы порекомендовал комбинацию проверки / выдачи исключений и журналов. Здесь анализ аварийных дампов кажется пустой тратой времени.

1
ответ дан 8 December 2019 в 03:08
поделиться

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

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

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

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

1
ответ дан 8 December 2019 в 03:08
поделиться
Другие вопросы по тегам:

Похожие вопросы: