Стандартный способ в C++ для определения класса исключений и создания исключений

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

Я думаю, было бы лучше определить исключение как вложенный класс, как это делается в библиотеке iostream с помощью ios_base::failure.

В чем я менее уверен, так это в том, где и как я должен определить объект my_excpetion. Хотел бы я увидеть внутренний код функций iostream и посмотреть, как они это делают. Я думал о нескольких вариантах:

  1. Для каждой причины исключения я могу определить статический экземпляр my_exception с конструктором, который получает строку и сохраняет ее в моем частном указателе строки.

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

  3. Я предполагаю, что это неправильно: каждый раз, когда я хочу сгенерировать исключение, создать новое my_exception с конструктором, который получает строку. Это делается в Java, но как я понимаю, это будет проблематично в C++, потому что исключение должно быть где-то удалено. Правильно?

Я думаю, что 1-й правильный, не так ли? Есть ли более стандартные варианты?

Большое тебе спасибо!


person ip84    schedule 28.05.2011    source источник
comment
хороший вопрос, но очень субъективный.   -  person iammilind    schedule 28.05.2011


Ответы (3)


Краткий ответ: вы захотите создавать исключения как объекты, а не как указатели. Вы поймаете их как ссылки.

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

Если вы ловите по указателю, catch (my_exception* e), то, глядя на него, вы не знаете, следует ли вам удалять память или нет.

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

Перехват по ссылке не имеет ни одной из этих проблем. Если вы пишете catch (my_exception& r), вы можете перехватывать полиморфные объекты, и вам не нужно беспокоиться об освобождении памяти.

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

Что касается других ваших производных классов исключений, это выбор стиля. Наследование от my_exception с другой реализацией what() довольно стандартно. Я бы не сказал, что вам нужно заморачиваться с хранением строк или экземпляров в статических объектах — они маленькие, и их создание практически не займет времени по сравнению с процессом раскручивания стека при возникновении исключения.

person jwismar    schedule 28.05.2011
comment
Он не будет скопирован, если вы поймаете его по ссылке, как вы рекомендуете. - person ildjarn; 28.05.2011
comment
обратите внимание, что если вы не планируете изменять исключение (добавление контекста и повторное создание), вам следует перехватить его по ссылке const. - person Matthieu M.; 28.05.2011
comment
@ildjarn Хороший вопрос. Я уточню это. Разве это не тот случай, когда при отсутствии оптимизации компилятора исходный временный файл, который вы создаете, копируется и уничтожается, и именно эта копия фактически перехватывается по ссылке? - person jwismar; 28.05.2011
comment
@Matthieu M. Это интересно, я раньше не слышал этой рекомендации. В чем преимущество делать это таким образом? Когда вы объявляете параметр функции как const &, вы получаете возможность связываться с r-значениями, и вы гарантируете вызывающему объекту, что не будете изменять переданный объект, но эти преимущества не применяются в ловильный блок. Недостатком (по общему признанию, чисто теоретическим) будет невозможность вызвать любые неконстантные члены для какого-либо произвольно брошенного объекта. - person jwismar; 28.05.2011
comment
На самом деле, невозможность изменить объект действительно применима :) Это говорит читателю, что вы не будете изменять причину исключения или добавлять какой-либо контекст. Должен признаться, я сам только недавно осознал эту возможность, поэтому моя кодовая база представляет собой несоответствие блоков ex& и ex const& catch, но я считаю, что последний вариант лучше. Хотя я немного const-зависим ;) - person Matthieu M.; 28.05.2011

Если вы наследуете от std::runtime_error, вам не нужно определять свой собственный элемент для хранения строки. Это делается за вас в std::exception (основа std::runtime_error). Не определено, как исключение сохраняет строку, но оно всегда должно работать.

#include <stdexcept>
#include <string>

struct MyException: public std::runtime_error
{
    MyException(std::string const& message)
        : std::runtime_error(message + " Was thrown")
    {}
};
person Martin York    schedule 28.05.2011
comment
+1 за идею, но, чтобы быть педантичным, std::exception не имеет конструктора для строк или хранилища, а std::runtime_error есть. (Однако MSVC поддерживает его в std::exception в любом случае.) - person GManNickG; 28.05.2011
comment
@GMan: Совершенно верно. Вот почему я использую std::runtime_error - person Martin York; 28.05.2011
comment
Хорошо, меня просто смутила эта часть: вам не нужно определять свой собственный член для хранения строки. Это делается для вас в std::exception; это не (обязательно) правда. - person GManNickG; 29.05.2011
comment
@GMan: технически верно. Это не определено стандартом, где сообщение сохраняется для любого из типов, производных от std::exception, и поэтому каждый тип может использовать свой собственный метод для хранения физического сообщения. Все, что мы знаем наверняка по std::runtime_error: Postcondition (of the constructor with a single std::string argument what_arg: strcmp(what(), what_arg.c_str()) == 0. - person Martin York; 31.05.2011

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

Вариант 1 был бы необычным, потому что обычно в нем нет необходимости.

Для варианта 2 вы должны создать экземпляр класса для выбрасывания. Невозможно выбросить тип, только экземпляр типа.

person Mark Ransom    schedule 28.05.2011
comment
Спасибо за замечание для общего случая. Хотя в моем простом случае, я думаю, конструктор копирования по умолчанию сработал бы. - person ip84; 28.05.2011