Исключения в конструкторе и вызов деструктора
Кто может указать ссылки на _место в стандарте_, где описано поведение исключений в конструкторе.
Есть такой класс
struct T
{
int *x;
void input()throw();
T(int *const ptr)throw():x(ptr){}
};
struct T1
{
int *a;
T b;
T2 c;
void print()const throw();
T()throw(exception&):a(0),b(0){ a=new int[10]; b.x=a;}
~T()throw(){ delete[] a; a=0; }
};
Вот его используем
try{ T obj; obj.b.input(); obj.print(); }catch(...){}
Вопросы:
1. Будет ли вызван деструктор, если произошла ошибка в инициализаторах конструктора T ( a(0),b(0),c() ), а конструктор, определенный программистом, выполняться не начинал;
2. Будет ли вызван деструктор, если конструктор T, определенный
программистом, начал выполняться и произошла ошибка в new.
Причина появления моего вопроса очень проста:
имеющаяся у меня информация не дает однозначного ответа на заданный мной вопрос о том, каковы правила вызова деструктора, если во время создания объекта (в конструкторе) выброшено исключение.
Некоторые отвечают на этот вопрос так:
если конструктор не был завершен (выбросил исключение), то деструктор не будет вызван, т.е. в конструкторе я должен сделать что-то такое:
/*
a,b,c,d - члены объекта, которым я выделяю в констукторе память
*/
T()
//инициализатор
:a(0),d(a), b(0),c(0)
//конструктор
{
a=alloc_mem();
if(!a)goto no_a;
b=alloc_mem();
if(!b)goto no_b;
c=alloc_mem();
if(!c)goto no_c;
//полезный код конструктора
...
if(some error)goto all_allocated;
...
if(other error)goto all_allocated;
...
return;
//дублирую в конструкторе код
деструктора
all_allocated:
free_mem(c); с=0;
no_c:
free_mem(b); b=0;
no_b:
free_mem(a); a=0;
no_a:
throw;
}
//деструктор
~T()
{
free_mem(c); с=0;
free_mem(b); b=0;
free_mem(a); a=0;
}
Другие говорят прямо противоположное:
если объект начал создаваться, то деструктор будет вызван в любом случае, т.е. даже если мой объект имеет члены-указатели на встроенный тип и инициализатор для них не был выполнен, то деструктор попытается освободить память, руководствуясь неопределенными значениями этих членов, это что-то такое:
/*
a,b,c,d - члены объекта, которым я выделяю в констукторе память
*/
T()
//инициализатор
:a(0),
d(a), //throw - здесь прервалась работа
конструктора
b(?),c(?) //неопределены
{
}
//деструктор
~T()
{
free_mem(c); с=0;
free_mem(b); b=0; //нельзя
free_mem(a); a=0; //нельзя
}
Третьи говорят что это зависит от реализации и стандартом не определено.
Однако, как трактует это все стандарт?
1. Если ошибка произошла при выделении памяти, то "вызываться" просто нечему (не проверял, потому что не мог сделать такую ошибку ;) )
2. Если ошибка произошла в "списке инициализации" класса: поведение создаваемого поля класса описывается, как для отдельного объекта, что же касается ранее созданных - не знаю :(
3. Если ошибка вообще произошла, то для базовых классов с завершёнными конструкторами деструкторы будут вызваны автоматически, в порядке, обратном вызову конструкторов, поля класса, в конструкторе которого произошла ошибка, также будут удалены корректно, а вот для создаваемых (new) объектов нужно будет прописывать delete вручную, в обработчике исключения конструктора.
Вывод, конструктор должен быть максимально простым, без генерации исключительных ситуаций. Если этого избежать невозможно (конструктор с параметрами, требующими проверки), то создание объектов класса нужно проводить после проверки.
{
void *a;
T() : a(0)
{
try
{
//something_which_can_throw_exception
a = alloc_mem();
}
catch(...)
{
if (a)
free_mem(a);
throw something;
}
}
}
?
Во-первых, неудобно дублировать деструктор в конструкторе руками.
Во-вторых, деструктор может быть все же вызван С++ компилятором, даже если исключение кинет инициализатор, т.е. не все члены класса успеют проинициализироваться, т.е. будет важен порядок инициализации.
Конструктор и деструктор это такие методы класса, которые обычно не вызываются программистом непосредственно, поэтому нужно точно знать правила, уникальные для С++, по которым они вызываются компилятором.
В-третьих, идея не выделять память в конструкторе, а выделять ее потом в (может быть виртуальных) методах create/destroy тоже не выход, т.к. неудобно будет использовать объекты, текст программы станет более трудночитаем.
Во-первых, неудобно дублировать деструктор в конструкторе руками.
ну, структурный подход ещё никто не отменял. выносим освобождение памяти в отдельный приватный метод, в деструкторе - его вызов, с конструкторе в кетче тоже. это не составит никаких сложностей при дальнейшем использовании этого обхекта
Во-вторых, деструктор может быть все же вызван С++ компилятором, даже если исключение кинет инициализатор, т.е. не все члены класса успеют проинициализироваться, т.е. будет важен порядок инициализации.
именно поэтому указатель изначально проинициализирован нулём ещё до того, как может возникнуть исключительная ситуация
а вообще, я тоже думаю что такие вещи зависят от релизации компилятора. мне говорили, что на сегодня любой компилятор C++ реализует стандарт языка максимум на 80%. компилятор C++ от Micro$oft реализует его на 60% (наверное это было сказано ещё про 6 студию)
[QUOTE=<SCORP>;155897]именно поэтому указатель изначально проинициализирован нулём ещё до того, как может возникнуть исключительная ситуация[/QUOTE]Кто же это по стандарту такой добрый, проинициализировал его нулем без меня?
Не знаю - Builder наверное :) . В настройках проекта есть такой флаг.
А вообще, лучше самому списки инициализации писать.
Кстати, порядок объектов-полей в списке роли не имеет. Ведь ошибки "выделения памяти" для поля объекта быть не может - объект создаётся в уже выделенной области памяти. А конструктор должен быть таким, чтобы исключения не генерировались - ввод "опасных" данных в принадлежащие объекты нужнопроводить внутри конструктора.
Нектоторые компиляторы, но далеко не все, действительно могут занулять всю память до вызова конструктора, не только указатели. При нормальном раскладе это только лишняя работа, хотя при плохом раскладе это единственная возможность безопасно создать объект. Где то я читал, что в некоторых компиляторах отличается вызов конструктора по умолчанию со скобочками/без скобочек, т.е. режим зануления управляется динамически в программе.
Очень даже имеет, если один объект инициализируется другим, но этот вопрос к этой теме в прямую не относится.
Вот это я не понял, если объект, вложенный в другой объект, имеет динамическую память (большинство сложных объектов такие), то он должен ее выделять себе сам и может бросить исключение. Да мало ли других причин, по которым он сожет бросить исключение? Исключение по причине нехватки памяти осложнено еще и утечкой памяти.
Это (ввод "опасных" данных...) практически невозможно.
1. Если попытаться прямо поручить выделение памяти "самому последнему, реально создающемуся объекту", то это нарушает инкапсуляцию, смысл создания объекта.
2. Если создать виртуальный интерфейс create/destroy и вызвать всю инициализацию, то это для простых объектов неудобно, да и вообще, это явно попытка избежать использования конструктора/деструктора только потому, что их поведение непонятно.
В С++ действительно можно избежать знания многих специфических особенностей языка и заменить их своими собственными соглашениями, но вот ctor/dtor как раз исключение из этого правила, уже писал почему.
посмотри внимательно в код и увидишь там инициализацию в конструкторе структуры ;)
Да мало ли других причин, по которым он сожет бросить исключение? Исключение по причине нехватки памяти осложнено еще и утечкой памяти.
[/quote]
1. Не учёл :(
2. Но вот "конструктор по умолчанию" по другим причинам бросить исключение не может - в противном случае, это очень плохой конструктор.
1. Если попытаться прямо поручить выделение памяти "самому последнему, реально создающемуся объекту", то это нарушает инкапсуляцию, смысл создания объекта.
2. Если создать виртуальный интерфейс create/destroy и вызвать всю инициализацию, то это для простых объектов неудобно, да и вообще, это явно попытка избежать использования конструктора/деструктора только потому, что их поведение непонятно.
1. VCL позволяет реализовать такую возможность, используя переопределение методов AfterConstruction и BeforeDestruction класса TObject - но это делается крайне редко, в основном эти методы используются для выполнения действий, которые должны происходить при создании/удалении, но выполняться над данными производных классов.
2. Как такое реализовать без TObject, мне не известно :(, потому что внутри конструктора/деструктроа виртуализация не работает.
Я долго старался узнать у кого-либо ответ о причинах такого поведения компилятора, но не смог ничего выяснить, так что лучше делать примерно так
class T
{
...
protected:
do_destruct()throw(){/* здесь код деструктора */}
public:
virtual ~T()throw(){do_destruct();}
T()throw(exception&):/* здесь код инициализации переменных - членов класса */
{try{
/* здесь код конструктора */
}catch(...){do_destruct(); throw;}}
};
Причем следует сделать так, чтобы do_destruct() мог бы быть безопасно вызван повторно и мог бы быть безопасно вызван для полностью проинициализированного, но частично сконструированного объекта.
В принципе, выбор вызова/невызова деструктора компилятором в какой то мере произволен, т.е. каждый вариант поведения имеет как плюсы, так и минусы.
Если бы сласс T был бы производным от класса A, то, в случае исключения в конструкторе T, деструктор для A вызвался бы автоматически.