Исключение из конструктора c

Обновлено: 22.04.2024

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


Или хранить их по ссылке:


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

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


Казалось бы, ничего не забыли. (Хотя, строго говоря, конечно забыли как минимум конструктор копирования и операцию присвоения, которые бы корректно работали с нашими указателями; ну да ладно.)

Воспользуемся этим классом:


И разберёмся, что и когда будет конструиваться и уничтожаться.

  • Сперва запустится процесс создания объекта Cnt.
  • В нём будет создан объект *xa
  • Начнёт создание объекта *xb.
  • … и тут произойдёт исключение

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

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

Какие же есть решения?

Самое простое, надёжное и естественное решение — хранить объект по значению


Это компактно, это элегантно, это естественно… но главное — это безопасно! В этом случае компилятор следит за всем происходящим, и (по-возможности) вычищает всё, что уже не понадобится.

Результат работы кода:


То есть объект Cnt::xa был автоматически корректно уничтожен.

Безумное решение с указателями

Настоящим кошмаром может стать вот такое решение:


Представляете, что будет, если появится Cnt::xc? А если придётся поменять порядок инициализации. Надо будет приложить не мало усилий, чтобы ничего не забыть, сопровождая такой код. И, что самое обидное, это вы сами для себя же разложили везде грабли.

Лирическое отступление про исключения.

Для чего были придуманы исключения? Для того, чтобы отделить описание нормального хода программы от описания реакции на какие-то сбои.

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

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

Это делает код запутанным и трудным для понимания и поддержки.

Решение настоящих индейцев — умные указатели

Если вам всё же необходимо хранить указатели, то вы всё равно можете обезопасить свой код, если сделаете для указателей обёртки. Их можно писать самим, а можно использовать множество, уже существующих. Пример использования auto_ptr:


Мы практически вернулись к решению с хранением членов класса по значению. Здесь мы храним по значению объекты класса auto_ptr, о своевременном удалении этих объектов опять заботится компилятор (обратите внимание, теперь нам не надо самостоятельно вызывать delete в деструкторе); а они, в свою очередь, хранят наши указатели на объекты X и заботятся о том, чтобы память вовремя освобождалась.

Да! И не забудьте подключить


Там описан шаблон auto_ptr.

Лирическое отступление про new

Одно из преимуществ C++ перед C состоит в том, что C++ позволяет работать со сложными структурами данных (объектами), как с обычными переменными. То есть C++ сам создаёт эти структуры и сам удаляет их. Программист может не задумываться об освобождении ресурсов до тех пор, пока он (программист) не начнёт сам создавать объекты. Как только вы написали «new», вы обязали себя написать «delete» везде, где это нужно. И это не только деструкторы. Больше того, вам скорее всего придётся самостоятельно реализовывать операцию копирования и операцию присвоения… Одним словом, вы отказались от услуг С++ и попали на весьма зыбкую почву.

Конечно, в реальной жизни часто приходится использовать «new». Это может быть связано со спецификой алгоритма, продиктовано требованиями по производительности или просто навязано чужими интерфейсами. Но если у вас есть выбор, то наверно стоит трижды подумать, прежде, чем написать слово «new».

Говорят, что конструктор конструктор и деструктор класса не должны вырабатывать исключения?
В тоже время, я видел огромное количество кода и даже примеров из книг, где в конструкторе выделяется память при помощи оператора `new`, но ведь даже он может вырабатывать исключение `bad_alloc` — выходит, что весь этот код потенциально опасный? Как с этим бороться?

Исключения, ровно как и оператор return прерывают поток выполнения команд функции, из системного стека выбираются объекты (такие как локальные переменные) и для них вызываются деструкторы. Однако, если при выполнении оператора return раскрутка стека прекратиться в точке где была вызвана завершенная функция, то при при выполнении throw объекты из стека будут уничтожаться до тех пор, пока управление не будет передано в блок try<> , содержащий обработчик, соответствующий типу выброшенного исключения. Читать подробнее про обработку исключений [1].

Исключения в деструкторе класса

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

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

destructor_exception_abort

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

Еще один из аспектов работы деструкторов и исключений иллюстрирует следующий фрагмент кода:

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

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

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

Исключения в конструкторе класса

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

Стандарт языка С++ гарантирует, что если исключение возникнет в конструкторе, то памяти из под членов-данных класса будет освобождена корректно вызовом деструктора — т.е. если вы используете идиому RAII [2], то проблем не будет. Часто для этого достаточно использовать std::vector/std::string вместо старых массивов и строк, и умные указатели вместо обычных [3]. Если же вы продолжите использовать сырые указатели и динамически выделять память — нужно будет очень тщательно следить за ней, например в следующем фрагменте кода нет утечки, т.к. исключение будет выработано только если память не будет выделена [4]:

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

Использование блоков try/catch/finally для восстановления после ошибок или высвобождения ресурсов

Используйте try / catch блоки вокруг кода, который может создать исключение , и код может восстановиться после этого исключения. В блоках catch следует всегда упорядочивать исключения от более производных к менее производным. Все исключения, производные от Exception. Более производные исключения не обрабатываются предложением catch, которому предшествует предложение catch для базового класса исключения. Если ваш код не удается восстановить после возникновения исключения, не перехватывайте это исключение. Включите методы выше по стеку вызовов для восстановления по мере возможности.

Очистите ресурсы, выделенные с помощью инструкций using или блоков finally . Рекомендуется использовать инструкции using для автоматической очистки ресурсов при возникновении исключений. Используйте блоки finally , чтобы очистить ресурсы, которые не реализуют IDisposable. Код в предложении finally выполняется почти всегда — даже при возникновении исключений.

Обработка общих условий без выдачи исключений

Для условий, которые могут возникнуть, но способны вызвать исключение, рекомендуется реализовать обработку таким способом, который позволит избежать исключения. Например, при попытке закрыть уже закрытое подключение возникает InvalidOperationException . Этого можно избежать, используя оператор if для проверки состояния подключения перед попыткой закрыть его.

Если состояние подключения перед закрытием не проверяется, исключение InvalidOperationException можно перехватить.

Выбор конкретного способа зависит от того, насколько часто ожидается возникновение данного события.

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

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

Устранение исключений при разработке классов

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

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

При выборе типа значения Nullable или значения по умолчанию в качестве индикатора ошибки учитывайте особенности приложения. При использовании Nullable default принимает значение null , а не Guid.Empty . В некоторых случаях добавление Nullable помогает более точно определить, присутствует или отсутствует значение. Но в определенных ситуациях добавление Nullable может привести к созданию лишних необязательных случаев для проверки, что повышает вероятность ошибки.

Выдача исключений вместо возврата кода ошибки

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

Создавайте новый класс исключений, только если предопределенное исключение не подходит. Пример:

Вызывайте исключение InvalidOperationException, если значение свойства или вызов метода не соответствуют текущему состоянию объекта.

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

Завершайте имена классов исключений словом Exception

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

Включение трех конструкторов в пользовательские классы исключений

Exception(), использующий значения по умолчанию.

Обеспечение доступности данных об исключении при удаленном выполнении кода

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

Поместите эту сборку в общую базу приложения, совместно используемую обоими доменами приложений.

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

Предоставление дополнительных свойств в пользовательских исключениях по мере необходимости

Размещение операторов throw для удобной трассировки стека

Трассировка стека начинается в операторе, породившем исключение, и завершается оператором catch , перехватывающим это исключение.

Использование методов построителя исключений

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

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

Восстановление состояния, если методы не выполняются из-за исключения

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

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

Один из способов обработки в этой ситуации заключается в перехвате всех исключений, выданных транзакцией начисления средств, и откате транзакции списания средств.

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

Исключения позволяют обозначить, что во время выполнения программы произошла ошибка. Создаются объекты исключений, описывающие ошибку, а затем создаются с ключевым словом throw . Далее среда выполнения ищет наиболее совместимый обработчик исключений.

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

Метод не способен выполнить свои заданные функции. Например, если значение параметра метода является недопустимым:

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

Когда аргумент метода вызывает исключение. В этом случае должно быть перехвачено исходное исключение и создан экземпляр ArgumentException. Исходное исключение должно передаваться конструктору ArgumentException в качестве параметра InnerException:

[! ПРИМЕЧАНИЕ]Приведенный выше пример предназначен для иллюстрационных целей. Проверка индекса с помощью исключений в большинстве случаев является плохой практикой. Исключения должны быть зарезервированы для защиты от исключительных условий программы, а не для проверки аргументов, как описано выше.

Исключения содержат свойство с именем StackTrace. Строка содержит имена методов в текущем стеке вызовов вместе с именем файла и номером строки, в которой было вызвано исключение для каждого метода. Объект StackTrace создается автоматически средой CLR из точки оператора throw , так что исключения должны вызываться из той точки, где должна начинаться трассировка стека.

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

Чего следует избегать при вызове исключений

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

Определение классов исключений

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

I'm having a debate with a co-worker about throwing exceptions from constructors, and thought I would like some feedback.

Is it OK to throw exceptions from constructors, from a design point of view?

Lets say I'm wrapping a POSIX mutex in a class, it would look something like this:

My question is, is this the standard way to do it? Because if the pthread mutex_init call fails the mutex object is unusable so throwing an exception ensures that the mutex won't be created.

Should I rather create a member function init for the Mutex class and call pthread mutex_init within which would return a bool based on pthread mutex_init 's return? This way I don't have to use exceptions for such a low level object.

Well it is ok to throw from ctors as much as it is from any other function, that being said you should throw with care from any function.

Something unrelated: why not removing your lock/unlock methods, and directly lock the mutex in the constructor and unlock in the destructor? That way simply declaring an auto variable in a scope automatically lock/unlock, no need to take care of exceptions, returns, etc. See std::lock_guard for a similar implementation.

If your construction fails and throws an exception, ~Mutex() will not be called and mutex_ will not be cleaned up. Don't throw exceptions in constructors.

11 Answers 11

Yes, throwing an exception from the failed constructor is the standard way of doing this. Read this FAQ about Handling a constructor that fails for more information. Having a init() method will also work, but everybody who creates the object of mutex has to remember that init() has to be called. I feel it goes against the RAII principle.

In most situations. Don;t forget things like std::fstream. On failure it still creates an object, but because we are always testing the state of the object normally it works well. So an object that has a natural state that is tested under normal usage may not need to throw.

Not really, in this specific case, note that his Mutex destructor will never be called, possibly leaking the pthread mutex. The solution to that is to use a smart pointer for the pthread mutex, better yet use boost mutexes or std::mutex, no reason to keep using old functional-style OS constructs when there are better alternatives.

@Martin York: I'm not sure std::fstream is a good example. Yes. It does rely on post-constructor error checking. But should it? It's an awful design that dates from a version of C++ where constructors were forbidden to throw exceptions.

If you do throw an exception from a constructor, keep in mind that you need to use the function try/catch syntax if you need to catch that exception in a constructor initializer list.

It should be noted that exceptions raised from the construction of a sub object can't be suppressed: gotw.ca/gotw/066.htm

Throwing an exception is the best way of dealing with constructor failure. You should particularly avoid half-constructing an object and then relying on users of your class to detect construction failure by testing flag variables of some sort.

On a related point, the fact that you have several different exception types for dealing with mutex errors worries me slightly. Inheritance is a great tool, but it can be over-used. In this case I would probably prefer a single MutexError exception, possibly containing an informative error message.

I'd second Neil's point about the exception heirarchy - a single MutexError is likely to be a better choice unless you specifically want to handle a lock error differently. If you have too many exception types, catching them all can become tiresome and error prone.

I agree that one type of mutex exception is enough. And this will also make error handling more intuitive.

the destructors are not called, so if a exception need to be thrown in a constructor, a lot of stuff(e.g. clean up?) to do.

You should be using a std::unique_ptr or similar. Destructor of members is called if an exception is thrown during construction, but plain pointers don't have any. Replace bar* b with std::unique_ptr b (you'll have to remove the delete b; and add the header), and run again.

This behavior is quite sensible. If the constructor has failed (was no successfully completed) why should the destructor be called? It has nothing to clean up and if did try to clean up objects which have not even been instantiated properly (think some pointers), it will cause a lot more problems, unnecessarily.

@zar Yes, the problem is not whether the destructor should be called or not. In this example, clean up should be done before throwing the exception. And I don't mean we cannot throw an exception in the constructor, I just mean the developer should known what he is dong. No good, no bad, but think before doing.

According to @Naveen's answer, it seems that the memory does freed. But valgrind --leak-check=full ./a.out complains block lost: ERROR SUMMARY: 2 errors from 2 contexts

It is OK to throw from your constructor, but you should make sure that your object is constructed after main has started and before it finishes:

The only time you would NOT throw exceptions from constructors is if your project has a rule against using exceptions (for instance, Google doesn't like exceptions). In that case, you wouldn't want to use exceptions in your constructor any more than anywhere else, and you'd have to have an init method of some sort instead.

Interesting discussion. My personal opinion is that you should use exceptions only when you actually design the program's error handling structure to take advantage of them. If you try to do error handling after writing the code, or try to shoehorn exceptions into programs that were not written for them, it's just going to lead to either try/catch EVERYWHERE (eliminating the advantages of exceptions) or to programs crashing out at the least little error. I deal with both every day and I don't like it.

Adding to all the answers here, I thought to mention, a very specific reason/scenario where you might want to prefer to throw the exception from the class's Init method and not from the Ctor (which off course is the preferred and more common approach).

I will mention in advance that this example (scenario) assumes that you don't use "smart pointers" (i.e.- std::unique_ptr ) for your class' s pointer(s) data members.

So to the point: In case, you wish that the Dtor of your class will "take action" when you invoke it after (for this case) you catch the exception that your Init() method threw - you MUST not throw the exception from the Ctor, cause a Dtor invocation for Ctor's are NOT invoked on "half-baked" objects.

See the below example to demonstrate my point:

I will mention again, that it is not the recommended approach, just wanted to share an additional point of view.

Also, as you might have seen from some of the print in the code - it is based on item 10 in the fantastic "More effective C++" by Scott Meyers (1st edition).

If your project generally relies on exceptions to distinguish bad data from good data, then throwing an exception from the constructor is better solution than not throwing. If exception is not thrown, then object is initialized in a zombie state. Such object needs to expose a flag which says whether the object is correct or not. Something like this:

Problem with this approach is on the caller side. Every user of the class would have to do an if before actually using the object. This is a call for bugs - there's nothing simpler than forgetting to test a condition before continuing.

In case of throwing an exception from the constructor, entity which constructs the object is supposed to take care of problems immediately. Object consumers down the stream are free to assume that object is 100% operational from the mere fact that they obtained it.

This discussion can continue in many directions.

For example, using exceptions as a matter of validation is a bad practice. One way to do it is a Try pattern in conjunction with factory class. If you're already using factories, then write two methods:

With this solution you can obtain the status flag in-place, as a return value of the factory method, without ever entering the constructor with bad data.

Second thing is if you are covering the code with automated tests. In that case every piece of code which uses object which does not throw exceptions would have to be covered with one additional test - whether it acts correctly when IsValid() method returns false. This explains quite well that initializing objects in zombie state is a bad idea.

Apart from the fact that you do not need to throw from the constructor in your specific case because pthread_mutex_lock actually returns an EINVAL if your mutex has not been initialized and you can throw after the call to lock as is done in std::mutex :

then in general throwing from constructors is ok for acquisition errors during construction, and in compliance with RAII ( Resource-acquisition-is-Initialization ) programming paradigm.

Focus on these statements:

  1. static std::mutex mutex
  2. std::lock_guard lock(mutex);
  3. std::ofstream file("example.txt");

The first statement is RAII and noexcept . In (2) it is clear that RAII is applied on lock_guard and it actually can throw , whereas in (3) ofstream seems not to be RAII , since the objects state has to be checked by calling is_open() that checks the failbit flag.

At first glance it seems that it is undecided on what it the standard way and in the first case std::mutex does not throw in initialization , *in contrast to OP implementation * . In the second case it will throw whatever is thrown from std::mutex::lock , and in the third there is no throw at all.

Notice the differences:

(1) Can be declared static, and will actually be declared as a member variable (2) Will never actually be expected to be declared as a member variable (3) Is expected to be declared as a member variable, and the underlying resource may not always be available.

All these forms are RAII; to resolve this, one must analyse RAII.

  • Resource : your object
  • Acquisition ( allocation ) : you object being created
  • Initialization : your object is in its invariant state

This does not require you to initialize and connect everything on construction. For example when you would create a network client object you would not actually connect it to the server upon creation, since it is a slow operation with failures. You would instead write a connect function to do just that. On the other hand you could create the buffers or just set its state.

Therefore, your issue boils down to defining your initial state. If in your case your initial state is mutex must be initialized then you should throw from the constructor. In contrast it is just fine not to initialize then ( as is done in std::mutex ), and define your invariant state as mutex is created . At any rate the invariant is not compromized necessarily by the state of its member object, since the mutex_ object mutates between locked and unlocked through the Mutex public methods Mutex::lock() and Mutex::unlock() .

Читайте также: