Исключительная безопасность - когда, как, почему? - PullRequest
10 голосов
/ 19 декабря 2010

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

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

Вот моя популярная функция:

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?(голова - первый узел)

Моя функция 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) Я часто слышу, что что-либо может не работать в программе на C ++.Реально ли проверить, не завершился ли конструктор ListElem (или tail_ во время new ing)?

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

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

Ответы [ 5 ]

8 голосов
/ 19 декабря 2010

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

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

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

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

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

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

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

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

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

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

Реально ли проверить, не завершился ли конструктор для ListElem (или tail_ при обновлении)?

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

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

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

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

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

6 голосов
/ 19 декабря 2010

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4 голосов
/ 19 декабря 2010

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

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

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

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

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

2 голосов
/ 19 декабря 2010

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

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

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

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

1 голос
/ 19 декабря 2010

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

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

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

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

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

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

...