Игра отражений
(КоТ: сорри, сир! Переопределив new под это дело (кстати, одно из упражнений в каком-то С++-учебнике), вполне возможно и вызвать. Только потом приходится delete лечить - он-то базируется на стандартных умолчаниях. То есть, данных объекта нет, и объекту адрес не выделен, но VMT его есть. В библиотеке или там где еще, не суть. И к этой VMT можно добраться через разную там… гм… CENSORED Плюс дельфы в откровенности доступа ко всем VMT проекта, независимо, созданы ли объекты соответствующих классов). Как правило, на этом месте фанаты С++ начинают кричать, что это де нелогично, так быть не должно… Но на самом деле нет ничего плохого в том, что конструктор использует в своей работе виртуальные принципы. Никто не утверждает, что экземпляр станет объектом С или В раньше, чем он станет объектом А. Конструкторы всего лишь выполняют свою работу, не важно в каком порядке. (КоТ: От слабости кричать. Т.к. это, безусловно, бонус дельфе перед С++, но и С++-модель определенные преимущества все-таки имеет). Более того, вызов указанной виртуальной функции совершенно бесполезен. Почему? В С++ при работе каждого из конструкторов A, B, C таблица виртуальных методов VMT будет соответствовать именно тому классу, к которому принадлежит конструктор. Т.е. вызов ЛЮБЫХ виртуальных методов в конструкторе С++ теряет всякий смысл, т.к. не является виртуальным (будет вызван соответствующий метод для класса А или В, а не для С). То же касается и деструкторов С++. В ОР при работе любого из конструкторов предков VMT всегда соответствует РЕАЛЬНОМУ создаваемому классу, т.е. классу С. Вызовы виртуальных методов будут правильными. В принципе, это может создать опасную ситуацию, когда в данном виртуальном методе какой-то из наследников подразумевает, что класс уже полностью сконструирован. Именно для разрешения этой проблемы и существует виртуальный метод AfterConstruction. Теперь мы четко видим, что конструкторы ОР обладают НАМНОГО большей гибкостью и мощью. Конечно, при условии, что программист понимает, что делает. (КоТ: при условии, что программист понимает, что делает, и С++ не так уж плох ;-) А это не так уж и сложно. По крайней мере, практика показывает, что эти конструкторы не доставляют никаких хлопот программистам. А значит, увеличение мощи не уменьшило "безопасности программирования" :) Продолжим.
C* c_;
D* d_;
public:
E(); // реализацию см.ниже
~E() { delete d_; delete c_; }
}
E::E() // конструктор класса E
try
: A(1), B(1), c_( new C ), d_( new D ) // список инициализации
{ //начало тела конструктора
cout<<"Constructor body";
} // конец тела конструктора
catch(...){ // ловим любое исключение
A::~A();
B::~B();
delete c_;
delete d_;
}
Непривычное написание, не так ли? Да, в Делфях нельзя ВЕСЬ процесс конструирования поместить в блок try except.
Неправильно! Скорее можно сказать, что в ОР нельзя НЕ поместить весь процесс конструирования в блок try…except. При вызове конструктора ОР как классового (статического, в терминах С++) метода (т.е. через классовую ссылку) блок try…except устанавливается АВТОМАТИЧЕСКИ. При возникновении любого необработанного исключения в конструкторе автоматически вызывается деструктор. Это, однако, не мешает вписать в тело конструктора свои блоки обработки исключений, в том числе и для полной, безопасной обработки некоторых их типов.
Здесь уместно более подробно осветить различия в способах вызова конструкторов ОР. Как я уже говорил, конструктору компилятором неявно передается параметр, который говорит, что он вызывается как классовый или как обычный метод.
В случае классового метода:
устанавливается блок try…except;
вызывается виртуальный метод NewInstance, выделяющий память под экземпляр класса. В случае переопределения Вами этого метода:
размер экземпляра можно получить методом InstanceSize;
память нужно очистить методом InitInstance;
отрабатывает тело конструктора;
вызывается виртуальный метод AfterConstruction.
В случае обычного метода выполняется только тело конструктора. Блок try…except НЕ устанавливается. Так вызываются все собственные конструкторы и конструкторы предков из тела какого-либо конструктора класса (они все равно попадут в установленный блок обработки исключений). Конструктор может быть вызван где угодно. Главное - использовать объектную ссылку (Self.Create), а не классовую. Условно, реальный код конструктора мог бы выглядеть так:
begin
if IsClassRef then
try
Self := TSomthing.NewInstance;
InitInstance(Self);
Self.Create(False); // Тело конструктора,
// написанное разработчиком
Self.AfterConstruction;
except
Self.Destroy; // Если что - харакири :)
end
else
Self.Create(False); // Тело конструктора
Result := Self;
end;
Аналогичная песня с деструкторами. Но здесь обойдемся без лишних объяснений:
begin
if Deallocate then
Self.BeforeDestruction;
Self.Destroy(False);
if Deallocate then
begin
Self.CleanupInstance;
Self.FreeInstance;
end;
end;
Еще раз замечу, что это чисто гипотетический код, создаваемый компилятором, а не реализация конкретного класса. Конечно, в нем нет никаких рекурсивных вызовов. Продолжим.
Но как же быть с динамическими ресурсами? Спросите вы. Все очень просто:
try
: A(1), B(1), c_( NULL ), d_( NULL ) // список инициализации
{ //начало тела конструктора
try{
c_ = new C;
d_ = new D;
cout<<"Constructor body";
}
catch(...){
if(c_) delete C;
if(d_) delete D;
throw;
}
} // конец тела конструктора
catch(...){ // ловим любое исключение
throw E_ErrorCreate();
}
Видно, что я использовал блок try...catch только для "перевода" одного исключения в другое. И назначение этого блока только такое и никакого другого. Использование его в других целях может привести к гадостям (КоТ: если ножом кухонным неправильно пользоваться, это МОЖЕТ привести даже к смерти… Но ведь не обязательно же приводит! Так претензии к ножу (языку), или к кривым рукам?) ,поэтому в некоторых С++ компиляторах (фирмы Борланд например) эта возможность от греха подальше убрана. Вы еще не заскучали?
Нет, Дмитрий, с Вами не соскучишься :)
Здесь хочу лишь заметить, что в ОР нет необходимости чистить ресурсы в конструкторе. На это есть деструктор! (КоТ: Вот!!!) Логично, не так ли? Зачем плодить двойной код. А вот конструкции вида
if Assigned(MyObject1) then
FreeAndNil(MyObject1);
if Assigned(MyObject2) then
FreeAndNil(MyObject2);
ОЧЕНЬ рекомендуется использовать именно в деструкторе. Это хороший стиль. (КоТ: что да, то да.) Конструкция аналогичная if (c_) delete c_ (кстати, здесь была ошибка).
(КоТ: с != 0 бывает, т.к NULL-тип машинно-зависимый. Но пустой указатель где-то представлен, напр, отрицательным числом. Если мне понадобилось, я бы писал
if (С ! = NULL) // что надо сделать с С
хотя Страуструп и советует использовать 0 вместо NULL - в третьей редакции книги. В первой, помнится, советовал обратное ;)
Ведь деструктор может быть вызван в любой момент работы конструктора, и часть ресурсов будет неинициализирована.
Привел я этот пример не для демонстрации возможностей блока try...catch, а для того чтобы показать как С++ сам делает безопасным процесс "конструирования" класса. В Делфи все это ложиться на хрупкие плЭчи программера.
(КоТ: "Врать не надо по телефону" (с) Булгаков)
Теперь мы видим, кто действительно "сам делает безопасным процесс конструирования класса", а кто перекладывает все это на чьи-то "хрупкие плЭчи".
Кстати о Делфях, я там не нашел аналог функции С++ - uncaught_exception() - показывает статус стека исключений. Благодаря этой функции ваш деструктор знает - нормальное это "устранение" класса или не нормальное. По-моему, очень даже пользительно.
Что значит ненормальное устранение класса? Может, мы еще будем считать возникновение исключения ненормальной ситуацией? Между прочим, на исключениях вполне можно выстроить логику работы класса или библиотеки. В ОР этому, кстати, очень способствуют такие преимущества модели исключений перед ANSI C++, как наличие общего предка исключений (и то, что это вообще классы, а не абы что) и наличие блока try…finally (ну это просто добавляет удобств по сравнению с try…catch(…){ throw; })
Поэтому не совсем понятно, зачем понадобилась некая функция uncaught_exception(). Зачем лезть в идеологию работы исключений со своим уставом? Ведь они как раз и избавляют разработчика от чрезмерного применения if. Это еще называется реактивной моделью программирования. Но, раз есть спрос, то есть и предложение:
ExceptAddr function - returns the address at which the current exception was raised.
ExceptObject function - returns a reference to the object associated with the current exception.
ExceptProc variable - points to the lowest-level RTL exception handler.
Фича в том, что оператор new уже выделил память для экземпляра класса TObject, а тут ррраз! И исключение! Что делает С++? Он тут же освободит память - не надо ставить блок try...catch. Все сделает С++. Ну, как говориться, приятная неожиданность.
(КоТ: это стандарт, и кто не знает его, как может говорить, что знает С++?)
Ну, это только для тех, кто не очень хорошо знает ОР и С++. Впрочем, такие "детские" неожиданности не избавляют программера в С++ от необходимости защиты динамических ресурсов. В ОР же в это время можно попить пива ;)
Я не привожу примеры реализации более полезных УМНЫХ указателей, реализующих сборку мусора и правильную работу с ресурсами вообще.
Судя по всему, автор прочитал книгу Джефа Элджера "C++", испестренную идеями УМНЫХ, ВЕДУЩИХ, ГЕНИАЛЬНЫХ указателей и сборки мусора. Здесь хочу заметить, что я иногда читаю книги с карандашом в руке. Это очень хорошая, умная (КоТ: ведущая и гениальная ;-) книга про С++. Только во время ее чтения, постоянно задумываешься, а как можно сделать тоже самое в ОР. В результате после прочтения книга превратилась в записную книжку, испестренную замечаниями о том, насколько проще и красивее выглядела бы в ОР большая часть предлагаемых решений. Для интересующихся - основная идея в использовании свойств и интерфейсов.
Отправить комментарий