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

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

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

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

Убедитесь, что каждый модуль согласованно использует одну и ту же внутреннюю стратегию обработки ошибок (предпочтительно — исключения С++; см. рекомендацию 72) и единую стратегию обработки ошибок в интерфейсе (например, коды ошибок для API на языке С); обе эти стратегии могут быть одинаковы, но обычно это не так. Стратегии обработки ошибок могут изменяться только на границах модуля. Определите, как происходит связывание стратегий между модулями (например, как происходит взаимодействие с COM или CORBA, или что всегда следует перехватывать исключения на границе с API на языке С). Хорошим решением будет определить центральные функции, которые выполняют преобразования между исключениями и кодами ошибок, возвращаемых подсистемой. Так вы сможете легко транслировать входящие ошибки от других модулей в используемые внутренние исключения и тем самым упростить интеграцию.

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

Небольшое предостережение. Некоторые операционной системы используют механизм исключений С++ при обработке низкоуровневых системных ошибок, как, например, разыменование нулевого указателя. Следовательно, инструкция catch(...) может перехватить больше исключений, чем вы ожидаете, так что ваша программа может оказаться в неопределенной ситуации при выполнении перехвата catch(...). Обратитесь к документации по вашей системе и либо приготовьтесь к обработке таких низкоуровневых исключений наиболее разумным способом, который сможете придумать, либо используйте в начале вашего приложения соответствующие системные вызовы для отключения такого поведения. Замена catch(...) последовательностью перехватов catch(E1&) {/*...*/} catch(E2&){/*.. .*/} ... catch(En&){/*...*/} для всех известных типов базовых исключений Ei масштабируемым решением не является, поскольку вам придется обновлять этот список при добавлении новых библиотек (использующих собственные иерархии исключении) в ваше приложение.

Использование catch(...) в других, не перечисленных в данной рекомендации местах зачастую является признаком плохого проектирования, поскольку означает, что вы хотите перехватить абсолютно все исключения без обязательного знания о том, как следует обрабатывать конкретные исключения (см. рекомендацию 74). В хорошей программе не так много перехватов всех исключений, да и вообще инструкций try/catch; в идеале ошибки распространяются через весь модуль, транслируются на его границе (неизбежное зло) и обрабатываются в стратегически размещенных местах.

Ссылки

[Stroustrup00] §3.7.2, §14.7 • [Sutter00] §8-17 • [Sutter02] §17-23 • [Sutter04] §11- 13

63. Используйте достаточно переносимые типы в интерфейсах модулей

Резюме

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

Обсуждение

Чем более широко распространяется ваша библиотека, тем меньше ваш контроль над средами программирования, используемыми вашими клиентами, и тем меньше множество типов, которые ваша библиотека может надежно использовать в своем внешнем интерфейсе. Взаимодействие между модулями включает обмен бинарными данными. Увы, С++ не определяет стандартные бинарные интерфейсы; широко распространенные библиотеки для взаимодействия со внешним миром могут полагаться на такие встроенные типы, как int и char. Даже один и тот же тип на одном и том же компиляторе может оказаться бинарно несовместимым при компиляции с разными опциями.

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

Требуется найти определенный компромисс между проблемами используемых типов, которые могут не быть корректно восприняты всеми клиентами, и проблемами использования низкого уровня абстракции. Абстракция важна; если некоторые клиенты понимают только низкоуровневые типы и вы ограничены в использовании этими типами, то, возможно, следует подумать о дополнительных операциях, работающих с высокоуровневыми типами. Рассмотрим функцию SummarizeFile, которая получает в качестве аргумента файл. Имеется три варианта действий — передать указатель char* на строку в стиле С с именем файла; передать string с именем файла и передать объект istream или пользовательский объект Filе. Каждый из этих вариантов представляет свой уровень компромисса.

• Вариант 1. char*. Очевидно, что тип char* доступен наиболее широкому кругу клиентов. К сожалению, это также наиболее низкоуровневый вариант; в частности, он более проблематичен (например, вызывающий и вызываемый код должны явно решить, кто именно выделяет память для строки и кто ее освобождает), более подвержен ошибкам (например, файл может не существовать), и менее безопасен (например, может оказаться подвержен классической атаке, основанной на переполнении буфера).

• Вариант 2. string. Тип string доступен меньшему кругу клиентов, ограниченному использованием С++ и компиляцией с использованием той же реализации стандартной библиотеки, того же компилятора и совместимых настроек компилятора. Взамен мы получаем менее проблематичный (надо меньше беспокоиться об управлении памятью; однако см. рекомендацию 60) и более безопасный код (например, тип string увеличивает при необходимости свой буфер и не так подвержен атакам на основе

Добавить отзыв
ВСЕ ОТЗЫВЫ О КНИГЕ В ИЗБРАННОЕ

0

Вы можете отметить интересные вам фрагменты текста, которые будут доступны по уникальной ссылке в адресной строке браузера.

Отметить Добавить цитату