Безопасность исключения - когда, как, почему?

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

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

Вот моя функция pop-front:

void List::pop_front()
{
    if(!head_)
        throw std::length_error("Pop front:  List is empty.\n");
    else
    {
        ListElem *temp = head_;
        head_          = head_->next;
        head_->prev    = 0;
        delete temp;
        --size_;
    }
}

У меня были некоторые дилеммы с этим.

1) Должен ли я действительно выдавать ошибку при сбое списка? Не следует ли мне просто ничего не делать и возвращать, а не принуждать пользователя списка выполнять try {] catch() {} (что тоже медленно).

2) Существует несколько классов ошибок (плюс ListException, которые мой учитель требует реализовать в классе). Является ли особый класс ошибок действительно необходимым для такой вещи, и существует ли общее руководство по использованию конкретного класса исключений? (Например, диапазон, длина и граница все одинаковые)

3) Я знаю, что я не должен изменять состояние программы до тех пор, пока не будет выполнен весь этот код, который сделал исключение. Вот почему я уменьшаю size_ last. Действительно ли это необходимо в этом простом примере? Я знаю, что удалить нельзя. Возможно ли, чтобы головка _- > prev когда-либо бросалась при назначении 0? (голова - первая Node)

Функция push_back:

void List::push_back(const T& data)
{
    if(!tail_)
    {
        tail_ = new ListElem(data, 0, 0);
        head_ = tail_;
    }
    else
    {
    tail_->next = new ListElem(data, 0, tail_);
    tail_ = tail_->next;
    }
    ++size_;
}

1) Я часто слышу, что что-то может сбой в программе на С++. Является ли реалистичным проверить, не сработает ли конструктор для ListElem (или tail_ во время new ing)?

2) Будет ли когда-либо понадобиться проверять тип данных (в настоящее время простой typedef int T, пока я не планирую все), чтобы убедиться, что тип является жизнеспособным для структуры?

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

Ответ 1

Должен ли я действительно выдавать ошибку при сбое списка? Не следует ли мне просто ничего не делать и возвращать, а не принуждать пользователя списка выполнять try {] catch() {} (что тоже медленно).

Абсолютно выбросить исключение.

Пользователь должен знать, что произошло, если список был пуст - иначе это будет ад для отладки. Пользователь не вынужден использовать инструкции try/catch; если исключение неожиданно (т.е. может произойти только из-за ошибки программиста), тогда нет причин пытаться его поймать. Когда исключение идет невостребованным, оно падает до std:: terminate и , это очень полезное поведение. В любом случае сами заявления try/catch не замедляют работу; какие издержки являются фактическим бросанием исключения и разворачиванием стека. Это ничего не стоит, если исключение не выбрасывается.

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

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

Я знаю, что я не должен изменять состояние программы до тех пор, пока не будет выполнен весь этот код, который сделал исключение. Вот почему я уменьшаю size_ last. Действительно ли это необходимо в этом простом примере? Я знаю, что удалить нельзя. Возможно ли, чтобы головка _- > prev когда-либо бросалась при назначении 0? (голова - первая Node)

Если head_ имеет значение null, то разыменование его (как часть попытки назначить head_->prev) - это undefined поведение. Выброс исключения - это возможное последствие поведения undefined, но маловероятное (требуется, чтобы компилятор изо всех сил старался держать вас за руку, на том языке, где такие вещи считаются абсурдными;)), и не о том, о чем мы беспокоимся, потому что поведение undefined - это поведение undefined - это значит, что ваша программа уже в любом случае ошибочна, и нет смысла пытаться сделать правильный путь неправильным.

Кроме того, вы уже явно проверяете, что head_ не равно null. Поэтому нет проблем, предполагая, что вы ничего не делаете с потоками.

Я часто слышу, что что-то может сбой в программе на С++.

Это немного параноидально.:)

Является ли реалистичным проверить, не сработает ли конструктор для ListElem (или tail_ во время новички)?

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

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

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

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

Придется ли вам когда-либо испытывать тип данных (в настоящее время простой typedef int T, пока я не планирую все), чтобы убедиться, что тип является жизнеспособным для структуры?

Типы проверяются во время компиляции. Вы не можете реально делать что-либо о них во время выполнения, и вам никогда не понадобится. (Если вам не нужна вся эта проверка типов, то почему вы используете язык, который заставляет вас вводить типы имен исключительно повсюду?:))

Ответ 2

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

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

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

Должен ли я действительно выдавать ошибку при сбое списка?

Это не "неудачный список", а именно pop_front вызывается, когда список пуст, который терпит неудачу. Вы не можете обобщать все операции над классом, чтобы они всегда делали исключения при отказе, вам нужно учитывать конкретные случаи. В этом случае у вас есть как минимум пять разумных вариантов:

  • возвращает значение, указывающее, было ли что-либо вытолкнуто. Caller может делать все, что им нравится, или игнорировать его.
  • чтобы поведение undefined вызывало pop_front, когда список пуст, а затем игнорируйте возможность в коде для pop_front. Это UB, чтобы заполнить пустой стандартный контейнер, а некоторые стандартные реализации библиотеки не содержат кода проверки, особенно в сборках релизов.
  • документируйте это поведение undefined, но все равно сделайте проверку, либо прервите программу, либо исключите исключение. Возможно, вы можете выполнить проверку только в отладочных сборках (для чего предназначена assert), и в этом случае у вас может также быть возможность запуска точки останова отладчика.
  • если список пуст, вызов не действует.
  • чтобы исключение выдавалось, если список пуст.

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

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

Является ли особый класс ошибок действительно необходимым для такой вещи

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

Возможно ли, когда head _- > prev будет когда-либо бросать при назначении 0?

Нет, если ваша программа каким-то образом спровоцировала поведение undefined. Итак, да, вы можете уменьшить размер до этого, и вы можете уменьшить его до delete при условии, что вы уверены, что деструктор ListElem не может выбрасывать. И при написании любого деструктора вы должны убедиться, что он не бросает.

Я часто слышу, что в программа на С++. Реально ли тестировать если конструктор для ListElem не работает (или tail_ во время новички)?

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

Вам не следует проверять, не сработает ли new, вы должны разрешить исключение из new, если оно есть, для распространения с вашей функции на вызывающего. Затем вы можете просто документировать, что push_front может бросать std::bad_alloc, чтобы указать отсутствие памяти, и, возможно, также, что он может выбросить все, что было создано конструктором копирования T (ничего, в случае int). Возможно, вам не нужно документировать это отдельно для каждой функции - иногда достаточно общего примечания, охватывающего несколько функций. Не должно быть неожиданным для всех, что если функция, называемая push_front, может бросать, то одна из вещей, которую она может бросить, - это bad_alloc. Это также не должно удивлять пользователей контейнера шаблонов, чем если бы исключенные исключения содержали элементы, то эти исключения могут распространяться.

Было бы необходимо проверить тип данных (в настоящее время простой typedef int T, пока я не буду templatize все), чтобы убедиться, что тип жизнеспособны для структуры?

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

Ответ 3

Это долгий вопрос. Я возьму все вопросы, которые пронумерованы 1).

1) Должен ли я действительно выдавать ошибку при сбое списка? Не следует ли мне просто ничего не делать и возвращать, а не принуждать пользователя списка выполнять try {] catch() {} (что тоже медленно).

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

1) Я часто слышу, что что-то может сбой в программе на С++. Является ли реалистичным проверить, не сработает ли конструктор для ListElem (или tail_ во время новички)?

Конструктор может, например, потерпеть неудачу, если у вас закончилась нехватка памяти, но в этом случае он должен выдать исключение, а не возвращать null. Поэтому вам не нужно явно проверять, чтобы конструктор не работал. См. Этот вопрос для получения дополнительной информации:

Ответ 4

Я часто слышу, что что-то может сбой в программе на С++. Является ли реалистичным проверить, не сработает ли конструктор для ListElem (или tail_ во время новички)?

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

В принципе, вы должны сигнализировать о сбое ЛЮБОЕ время, когда код не может полностью выполнить то, что заявляет его API.

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

Ответ 5

Для первого набора вопросов:

  • Да, вы должны бросить, по всем причинам в ответ @Mark. (+1 к нему)
  • Это не обязательно, но это может сделать жизнь ваших абонентов намного проще. Одним из преимуществ обработки исключений является то, что он локализует код для совместного использования определенного класса ошибок в одном месте. Выбирая конкретный тип исключения, вы позволяете своим вызывающим абонентам специально улавливать эту ошибку или быть более общими, захватывая суперкласс выбранного вами исключения.
  • Все инструкции в else предоставляют гарантию nothrow.

Для вашего второго набора:

  • Нет, это не реально проверить. Вы не знаете, что может сделать базовый конструктор. Это может быть ожидаемый элемент (т.е. std::bad_alloc), или это может быть что-то странное (т.е. int), и поэтому единственный способ, которым вы могли бы справиться с ним, - поместить его внутри catch(...), который является злом:)

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

  • Предположим, что любая операция на T может быть выбрана, кроме деструктора.