Обучающие курсы:

Обучение профессии "Разработчик C#" + стажировка в Mail.ru
Обучение профессии "Разработчик Python" + трудоустройство
Обучение профессии "Веб-разработчик" + стажировка в Mail.ru


Главная страница
Библиотека (скачать книги)
Скачать софт
Введение в программирование
Стандарты для C++
Уроки по C#
Уроки по Python
HTML
Веб-дизайн
Ассемблер в среде Windows
ActiveX
Javascript
Общее о Линукс
Линукс - подробно
Линукс - новое
Delphi
Паскаль для начинающих
Турбопаскаль
Новости
Партнеры
Наши предложения
Архив новостей







73. Генерируйте исключения по значению, перехватывайте — по ссылке

Резюме

Генерируйте исключения по значению (не через указатель) и перехватывайте их как ссылки (обычно константные). Эта комбинация наилучшим образом соответствует семантике исключений. При повторной генерации перехваченного исключения предпочтительно использовать просто оператор throw;, а не инструкцию throw e;.

Обсуждение

При генерации исключения генерируйте его по значению. Избегайте использовать исключение-указатель, поскольку в этом случае вам придется столкнуться с массой вопросов управления памятью: вы не можете генерировать исключение-указатель на значение в стеке, поскольку до того, как указатель достигнет точки назначения, стек будет свернут. Вы можете генерировать исключение-указатель на динамически выделенную память (если, конечно, ошибка, о которой вы сообщаете, не состоит в нехватке памяти), но при этом вы возлагаете на catch-блок задачу по освобождению выделенной памяти. Если вы уверены, что вам надо генерировать именно указатель, подумайте, нельзя ли заменить его интеллектуальным указателем типа shared_ptr<T> вместо обычного T*.

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

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

При повторной генерации исключения e лучше просто писать оператор throw; вместо инструкции throw e;. Дело в том, что первый способ всегда сохраняет полиморфизм объекта повторно генерируемого исключения.

Примеры

Пример. Повторная генерация измененного исключения. При повторной генерации перехваченного исключения предпочтительно использовать простой оператор throw;.

catch( MyException& e ) {        // Перехват неконстантной  ссылки
    e.AppendContext("Перехват"); // Внесение изменения
    throw;                       // Повторная генерация
}                                // модифицированного исключения

74. Уведомляйте об ошибках, обрабатывайте и преобразовывайте их там, где следует

Резюме

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

Обсуждение

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

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

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

  • Для добавления высокоуровневого семантического значения. Например, в текстовом редакторе функция Document::Open может принимать низкоуровневую ошибку "неожиданное окончание файла" и преобразовывать ее в ошибку "неверный или поврежденный документ", добавляя соответствующую семантическую информацию.

  • Для изменения механизма обработки ошибок. Например, в модуле, который внутренне использует исключения, но чей API в стиле C сообщает об ошибках посредством кодов ошибок, функции API должны перехватывать исключения и возвращать документированные коды ошибки, понятные вызывающему коду.

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

Исключения

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

75. Избегайте спецификаций исключений

Резюме

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

Обсуждение

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

Спецификации исключений не являются частью типа функции за исключением тех случаев, когда они таки ею являются. Это не шутка — они по сути образуют "теневую" систему типов, из-за чего написание спецификаций исключений в разных местах программы оказывается:

  • неверным — в инструкции typedef для указателя на функцию;

  • разрешенным — в точно таком же коде, но без использования typedef;

  • необходимым — в объявлении виртуальной функции, которая перекрывает виртуальную функцию базового класса со спецификацией исключений;

  • неявным и автоматическим — в объявлении конструкторов, операторов присваивания, операторов присваивания и деструкторов, когда они неявно генерируются компилятором.

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

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

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

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

Снижение производительности в обмен на введение ограничений практически всегда бесполезно. Это отличный пример преждевременной пессимизации (см. рекомендацию 9).

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

Исключения

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

class Base { //...      // В чужом классе имеется
    virtual f()         // спецификация исключений,
        throw(X, Y, Z); // и если вы не можете
};                      // ее удалить...

class MyDerived
: public Base  { //...  // ... то в вашем классе при
    virtual f()         // перегрузке функции она должна
        throw(X, Y, Z); // иметь совместимую (желательно
};                      // идентичную) спецификацию исключений

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

STL: контейнеры

Если вам нужен контейнер, по умолчанию используйте vector.
— Бьярн Страуструп (Bjarne Stroustrup),

Мы знаем, что вы уже предпочитаете использовать стандартные контейнеры вместо написанных вручную. Но какой именно контейнер следует использовать? Что должно (и что не должно) храниться в контейнерах и почему? Как следует заполнять контейнеры? Какие важные идиомы надо знать?

В этом разделе имеются ответы на все перечисленные (и не только) вопросы. И не случайно, что три первые рекомендации в нем содержат слова "Используйте vector...".

В этом разделе мы считаем наиболее значимой рекомендацию 79 — "Храните в контейнерах только значения или интеллектуальные указатели". К ней мы добавим — если даже вы не планируете применять для иных целей, все равно воспользуйтесь их интеллектуальным указателем shared_ptr.

76. По умолчанию используйте vector. В противном случае выбирайте контейнер, соответствующий задаче

Резюме

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

Если конкретных предпочтений нет, возьмите vector и спокойно работайте, зная, что вы сделали верный выбор.

Обсуждение

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

  • Следует писать в первую очередь корректный, простой и понятный код (см. рекомендацию 6). Предпочтителен выбор контейнера, который позволит вам написать наиболее понятный код. Так, если вам требуется вставка в определенную позицию, используйте последовательный контейнер (например, vector, list). Если вам нужен итератор произвольного доступа, выберите vector, deque или string. Если нужен словарный поиск наподобие c[0] = 42;, воспользуйтесь ассоциативным контейнером (например, set, map) — но если вам требуется упорядоченная ассоциативная коллекция, то вы не можете использовать контейнеры с использованием хеширования (нестандартные hash_... или стандартные unordered_... контейнеры).

  • Вопросы эффективности при написании должны учитываться только тогда и там, где это действительно необходимо (см. рекомендацию 8). Если на основании проведенных измерений с реальными данными доказано, что скорость поиска действительно критична, можно воспользоваться контейнерами с использованием хеширования (нестандартными hash_... или стандартными unordered_... контейнерами), затем — отсортированным vector, затем — set или map, обычно в приведенном порядке. Даже в этой ситуации отличия асимптотического времени работы (с использованием "большого О", например, линейное и логарифмическое время работы; см. рекомендацию 7) имеет значение, только если контейнеры достаточно велики, чтобы постоянные множители перестали играть роль. Для контейнеров, содержащих небольшие объекты наподобие double, для преодоления влияния постоянных множителей обычно требуется по крайней мере несколько тысяч элементов.

  • Там, где это возможно и разумно, лучше писать транзакционный код со строгой гарантией безопасности (см. рекомендацию 71), и не использовать некорректные объекты (см. рекомендацию 99). Если для вставки и удаления элементов вам требуется транзакционная семантика, или если необходимо минимизировать недействительность итераторов, лучше воспользоваться контейнерами на основе узлов (например, list, set, map).

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

Если вы сомневаетесь в данном совете, спросите сами себя — действительно ли у вас есть непреодолимые причины не использовать стандартный контейнер vector, который обладает следующими свойствами.

  • Гарантирует минимальные среди всех контейнеров накладные расходы памяти на хранение (ноль байтов на объект).

  • Гарантирует наивысшую среди всех контейнеров скорость доступа к хранимым элементам.

  • Гарантирует локальность ссылок, означающую, что объекты, находящиеся рядом друг с другом в контейнере, гарантированно находятся рядом и в памяти, что не гарантирует ни один другой контейнер.

  • Гарантирует совместимость размещения в памяти с размещением, используемым языком программирования С, в отличие от остальных стандартных контейнеров (см. рекомендации 77 и 78).

  • Гарантирует наличие самых гибких итераторов (произвольного доступа).

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

Есть ли у вас причины не использовать такой контейнер по умолчанию? Если у вас есть такая причина, связанная с одним из трех аспектов в начале этой рекомендации, то все замечательно — просто воспользуйтесь наиболее подходящим для вас контейнером. Если такой причины нет — возьмите vector и спокойно работайте, зная, что вы сделали верный выбор.

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

Примеры

Пример. Использование vector для небольших списков. Распространенное заблуждение — использовать list только потому, что "очевидно, что list — подходящий тип для выполнения операций со списком", таких как вставка в средину последовательности. Тип vector для небольших списков практически всегда превосходит тип list. Несмотря на то, что вставка в средину последовательности требует линейного времени работы у vector, и постоянного — у list, вектор с небольшим количеством элементов обычно справляется с этой задачей быстрее — за счет меньшего постоянного множителя; преимущества асимптотического времени работы list проявляются только при больших количествах элементов в контейнере.

Таким образом, используйте vector, пока размеры данных не потребуют иного выбора (см. рекомендацию 7), либо пока не станет существенной обеспечение строгой гарантии безопасности при возможной генерации исключений копирующим конструктором или копирующим оператором присваивания типа объектов, хранящихся в контейнере. В последнем случае может оказаться важным, что контейнер list обеспечивает строгую гарантию безопасности для операции вставки в коллекцию таких типов.



 
 

Библиотека программиста. 2009.
Администратор: admin@programmer-lib.ru