Главная страницаОбратная связьКарта сайта

Игра отражений

Оформил: DeeCo

Автор: Владимир Волосенков

Музыку любите, а на инструменте неприличное слово нацарапали.
"Республика ШКИД"

Данный материал является независимым дополнением/исправлением к статье Дмитрия Логинова "ЯП, ОПП и т.д. и т.п. в свете безопасности программирования". Поводом к написанию явилось наличие в исходном материале множества неточностей и откровенно ложных сведений, вводящих в заблуждение неподготовленного читателя.

Условно материал Дмитрия можно разделить на две части: историческую и непосредственно техническую. По исторической части у меня вопросов нет, и прочитал я ее с большим интересом. Целью данного материала является внесение ясности по техническим вопросам в меру моих скромных знаний.
Исходный текст я буду приводить курсивом. Т.к. в статье в основном сравнивается C++ и Delphi, то вместо Pascal или Object Pascal будет использоваться сокращение ОР. Т.к. автор в своем повествовании не ограничивался сравнением только безопасности программирования в С++ и ОР, то я также позволю себе сравнения по всем аспектам. Конечно, только в рамках технических фактов.

Кроме того, в скобках иногда будут встречаться комментарии за подписью КоТ. Это замечания одного непрофессионального программиста по поводу моих и Дмитрия размышлений. Пишет он в С++ и исключительно под Linux, называет себя не иначе, как глупым ламером. Впрочем, исходя хотя бы из того, что обычно настоящие ламеры себя таковыми не считают, его высказывания весьма интересны и часто к месту. Итак, приступим.

Сразу замечу, что размер страницы памяти для процессоров Intel и MIPS составляет 4К, а для Alpha - 8K (а не 2 и 4К соответственно).

Начнем с принципиальных отличий в модели обработки исключений в С++ от Делфи. И какие это порождает гадости (КоТ: почему именно гадости?). В первую очередь, Борланд ввел некоторые ограничения на перегенерацию собственных исключений. Вырезка из Help:
1) You cannot rethrow an operating system exception once the catch frame has been exited and have it be caught by intervening VCL catch frames.
2) You cannot rethrow an operating system exception once the catch frame has been exited and have it be caught by intervening operating system catch frames.
3) You cannot use "throw"(аналог Делфийского raise) to reraise an exception that was caught within VCL code.

Приведенная автором вырезка в Delphi Help отсутствует. Да и с какой стати там будет указываться ключевое слово "throw" из С++? Более всего это похоже на вырезку из хелпа C++ Builder. Соответственно и ограничения на работу с более мощной моделью исключений ОР, используемой в VCL (доказательства будут ниже). Выводы делаем сами…

Рассматривая модель ООП в Делфях и модель ООП в С++, легко прийти к выводу, что функционально модель С++ шире, и поэтому Борландовский Буилдер легко "глотает" делфийский VCL.

Используя модель ООП С++, создать, к примеру, среду Delphi или библиотеку VCL невозможно в принципе (если не касаться разработки новых компиляторов). Это было неоднократно доказано в дискуссиях с другими фанатами С++. Как ограничения выступают отсутствие классовых ссылок, виртуальных конструкторов и ущербная модель RTTI в С++. Не буду утверждать, как работает C++ Builder, но подозреваю, что ключевые моменты работы среды с компонентами написаны на ОР.

Думаю, если бы С++ позволял написать VCL, то Delphi пришлось бы сейчас "глотать" чужой код. Но пока все наоборот. Кстати, Borland имела прекрасную возможность пересмотреть свои воззрения на языковую основу VCL при разработке Kylix (этот проект включает и ОР и С++). Однако революции не произошло. Революция уже случилась в 95-м году с выходом Delphi 1 :)

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

(КоТ: Никакого плюса не вижу, говорю как С++ - программер. Мало геморроя с распределением динамической памяти, так еще и со всеми другими. Из-за этого я на линух от доса перешел - кстати. И вообще, на мой (ламерский) взгляд, распределение памяти - вопрос не к языку, а к мемори-модели операционной системы.)

В Делфи классы (объекты) могут располагаться только в динамической памяти

Что, безусловно, добавляет той самой безопасности программирования. К примеру, функция может вернуть ссылку на объект в стеке, который уже уничтожен. (КоТ: такие ошибки у меня часто были в досовском паскале. Именно тогда я привык инициализировать все переменные в процессе декларирования.) Если Дмитрий интересовался вопросами сборки мусора, то не мне ему объяснять, что сделать это в одной динамической памяти куда легче.

Из этого вытекает следующее отличие. Все конструкторы и деструкторы классов Паскаля вызываются явно.
object := TMyObject.Create. // где-то в начале
  //....
object.Free; // где-то в конце
С одной стороны хорошо. Все ясно, как никогда. Но это специфика Паскаля заставляет программера делать уйму работы, и порой ошибаться
(КоТ: а что, С++ прямо так вот и гарантирует безошибочность?) Частенько бывает необходимо иметь "неявный" вызов или конструктор "по умолчанию". Конструктор класса С++, например, вызывается как только встречается описание экземпляра (переменной) класса. И, соответственно, деструктор вызовется, как только класс "выйдет из области видимости".

(КоТ: опять подмена терминов. Т.е. банально нечестная игра. Справедливей, имхо, сказать, что в определенных задачах приходится не надеяться на механизм порождения классов дельфы. Но ведь и в С++ есть точно такие же ситуации - где-то ты можешь положиться на язык (компилятор), где-то - не можешь. Так в чем же преимущество?)

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

А что касается области видимости класса и времени жизни, то это элементарно организуется использованием интерфейсов. Всю работу по подсчету количества ссылок и автоматическому менеджменту памяти возьмет на себя Delphi. (КоТ: кстати, в том же С++ такой же механизм я сам организовывал часов за восемь. Не скажу, что просто и легко, но возможно. Минус - лишний геморрой, плюс - можешь сделать сам, какой нужно, с точностью до битовых полей и регистров.) В Delphi этот механизм также может быть легко реализован по-своему.

К тому же, такая форма конструирования имеет под собой четкую логическую основу. Она напрямую ориентированна на использование классовых ссылок, когда вместо статического указания типа (TMyObject) используется переменная типа "тип класса":
TComponentClass = class of TComponent; //ссылка на класс

function CreateAny(AType: TComponentClass): TComponent;
begin
  Result := AType.Create(nil);
end;
…
Form1.InsertComponent(CreateAny(TButton));
// Создали кнопку
Form1.InsertComponent(CreateAny(Edit1.ClassType));
// Создали еще одно поле редактирования
Скажем прямо, такие решения в С++ недоступны. В качестве лирического отступления можно сказать, что именно на этом основана работа Delphi IDE с любым компонентом.

Вас не удивляло, что Delphi способна не то что без перекомпиляции, а даже без перезапуска брать внешние, абсолютно не знакомые ей классы (компоненты, которые можно инсталлировать хоть каждые 5 минут, тип которых, конечно, неизвестен) и строить на их основе другие классы (формы и т.д.) в run-time (для разработчика design-time)?

(КоТ: круто, конечно)

Очень занимательный вопрос, скажу я вам. Прикиньте, как бы вы реализовали это в своем приложении. Механизм должен быть очень универсальным, работающим для любого компонента. Компоненты поставляются, например, в виде DLL (или packages - разновидность DLL). Тут никакая RTTI в чистом виде не поможет. Применительно к этой задаче даже шаблоны С++ абсолютно бесполезны, т.к. они являются механизмом compile-time only.

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

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

TComplex* c; // переменная указатель на тип TComplex - ниче не вызывается.

(КоТ: ну кто же в софтине будет САМ создавать указатель на пустое место? Зачем? Чтобы stack error"ом по хоботу получить? Объявил переменную - инициализируй!!! Вот так:
TСomplex*  c=new TComplex(1,1)
// "а будешь делать не так, надеру уши" (с) Зеф, "Обитаемый остров")

c = new TComplex(1,1); // выделяется память под TComplex и вызывается его конструктор с параметрами.
delete c; // освобождаем память предварительно вызвав деструктор Tcomplex
Вот здесь работа с классами похожа на Делфийскую работу. Похожа-то, похожа - да не совсем.

. (КоТ: на CENSORED похожа, да и работы я здесь не вижу что-то.)

Во-первых: как вы успели заметить new и delete - это операторы. Значит их можно переопределять (КоТ: кстати, НЕ ВСЯКИЙ оператор С++ переопределяется). Значит, где захочу - там и будут лежать мои классы. Так можно организовать несколько куч, даже не имея "много-кучевого" менеджера ОС. Я позже опишу, как это влияет на безопасность

Странно, но Дмитрию не известно, что управление памятью классов в ОР реализовано даже не с помощью операторов, а гораздо красивее - на уровне TObject, виртуальными (!) методами NewInstance и FreeInstance. Таким образом, абсолютно ЛЮБОЙ класс может переопределить эти методы для осуществления желания "где хочу - там и буду лежать". Соответственно организуется и "многокучность".

Во-вторых: здесь всплывает понятие "ВРЕМЯ ЖИЗНИ КЛАССА" и то, как обрабатываются исключения в конструкторах и деструкторах. Рассмотрим это поближе. В Делфи время жизни класса таково:
  • Рождение: Класс начинает свое существование сразу ПОСЛЕ окончания работы КОНСТРУКТОРА(вызов AfterConstruction).
  • Смерть: Класс заканчивает свое существование сразу ПОСЛЕ окончания работы ДЕСТРУКТОРА(вызов BeforeDestruction).

Неправильно. Before он на то и Before, чтобы отрабатывать ДО вызова деструктора. И это вовсе не значит, что класс уже уничтожен. Для справки: BeforeDestruction введен для того, чтобы создатель класса был уверен, что необходимые действия перед его уничтожением будут выполнены всегда, независимо от того, вызовут или нет его потомки унаследованный деструктор. По поводу AfterConstruction разговор будет чуть позже.

Кроме того, и конструкторы и деструкторы в ОР имеют приятную особенность (и далеко не одну). Они могут вызываться как обычные методы. Для этого в них передается неявный параметр. Не путать с неявным Self или this. Кстати, Self в классовых методах ОР является классовой ссылкой, а не объектной.

Так что вопросы рождения и смерти в ОР далеко не так тривиальны. Впрочем, самые интересные подробности еще впереди.

В С++ немножечко по другому:
  • Рождение: Сразу ПЕРЕД телом конструктора.
  • Смерть: Сразу ПОСЛЕ тела деструктора.
Это несколько меняет работу с конструкторами/деструкторами родителями и конструкторами/деструкторами членами. Вот С++:
   class TChild : public TMama,TPapa{ // :o)
     TMemberOne member_1_;
     TMembarTwo member_2_;
   public:
     TChild() { cout<<"TChild created!"; }
   }
Порядок конструкторов будет следующий: TMama, TPapa, TMemberOne, TMemberTwo и только потом вызовется ТЕЛО конструктора TChild. Это логично и похоже на правду.
(КоТ: немножко беременной быть можно? Это похоже на правду, или это правда? Разницу чувствуете?) Действительно, когда мы можем получить доступ к методам и полям(переменным класса) родителей и классов-членов(конкретных классов)? Мы можем получить этот доступ только, когда они сконструированы. И это лучше оставить на совести компилятора, чем надеяться на программера.

Вообще типичной идеологией компилятора С++ считается: "Ну, парень, если ты хочешь сделать именно так, делай, а я умываю руки". А тут такая удивительная забота о программере! Только вот она в данном случае совсем не к месту, по крайней мере, в таком виде. Как контраст - конструкторы ОР.

Допустим, у нас есть иерархия классов A -> B -> C. Мы конструируем класс С. Действительно, в С++ последовательность конструирования будет A -> B -> C. И никак иначе.

Теперь признайтесь, когда вы пишите конструктор в ОР, вы ведь первым делом указываете вызов inherited. Да? В этом случае последовательность конструирования абсолютно аналогична. Но! Стоит вам убрать inherited, и Delphi будет конструировать класс C в последовательности C -> B -> A. Неплохо для начала, но это еще цветочки.

Незаметное inherited дает вам полный контроль над тем КАК, КОГДА и КАКИЕ конструкторы будут вызываться (и будут ли вызываться вообще, ведь inherited можно и в if засунуть). Нет никаких ограничений на расположение inherited в теле конструктора. А ведь его еще можно дополнить именем конкретного конструктора предка с указанием нужных параметров. Ну и, конечно, можно вызывать собственные конструкторы. (КоТ: это здорово, однако).

Таким образом, сначала, например, может отработать часть конструктора С, затем конструкторы предков, затем оставшаяся часть конструктора С. Для чего все это?

Прозаический пример. В конструкторе С создается некоторый объект (аллокатор памяти, например), который используется для работы в конструкторе предка B. Другой наследник, класс D, может создавать совершенно другой объект. Создание этого объекта можно вынести в виртуальную функцию, которую вызывать перед inherited.

Да, такие возможности используются не слишком часто, но им есть реальное применение. Показательно, что подобный подход нереализуем в С++ никакими способами. Он никогда не даст создать что-то ПЕРЕД работой конструктора предка.

(КоТ: сорри, сир! Переопределив new под это дело (кстати, одно из упражнений в каком-то С++-учебнике), вполне возможно и вызвать. Только потом приходится delete лечить - он-то базируется на стандартных умолчаниях. То есть, данных объекта нет, и объекту адрес не выделен, но VMT его есть. В библиотеке или там где еще, не суть. И к этой VMT можно добраться через разную там… гм… CENSORED Плюс дельфы в откровенности доступа ко всем VMT проекта, независимо, созданы ли объекты соответствующих классов).

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

(КоТ: От слабости кричать. Т.к. это, безусловно, бонус дельфе перед С++, но и С++-модель определенные преимущества все-таки имеет).

Более того, вызов указанной виртуальной функции совершенно бесполезен. Почему? В С++ при работе каждого из конструкторов A, B, C таблица виртуальных методов VMT будет соответствовать именно тому классу, к которому принадлежит конструктор. Т.е. вызов ЛЮБЫХ виртуальных методов в конструкторе С++ теряет всякий смысл, т.к. не является виртуальным (будет вызван соответствующий метод для класса А или В, а не для С). То же касается и деструкторов С++.

В ОР при работе любого из конструкторов предков VMT всегда соответствует РЕАЛЬНОМУ создаваемому классу, т.е. классу С. Вызовы виртуальных методов будут правильными. В принципе, это может создать опасную ситуацию, когда в данном виртуальном методе какой-то из наследников подразумевает, что класс уже полностью сконструирован. Именно для разрешения этой проблемы и существует виртуальный метод AfterConstruction.

Теперь мы четко видим, что конструкторы ОР обладают НАМНОГО большей гибкостью и мощью. Конечно, при условии, что программист понимает, что делает. (КоТ: при условии, что программист понимает, что делает, и С++ не так уж плох ;-) А это не так уж и сложно. По крайней мере, практика показывает, что эти конструкторы не доставляют никаких хлопот программистам. А значит, увеличение мощи не уменьшило "безопасности программирования" :) Продолжим.
class E: public A,B {
     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), а не классовую. Условно, реальный код конструктора мог бы выглядеть так:
function TSomething.Create(IsClassRef: boolean): TSomething;
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;
Аналогичная песня с деструкторами. Но здесь обойдемся без лишних объяснений:
procedure TSomething.Destroy(Deallocate: boolean);
begin
  if Deallocate then
    Self.BeforeDestruction;
  Self.Destroy(False);
  if Deallocate then
  begin
    Self.CleanupInstance;
    Self.FreeInstance;
  end;
end;

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

Но как же быть с динамическими ресурсами? Спросите вы. Все очень просто:
   E::E() // конструктор класса E
   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. Это еще называется реактивной моделью программирования. Но, раз есть спрос, то есть и предложение:
  1. ExceptAddr function - returns the address at which the current exception was raised.
  2. ExceptObject function - returns a reference to the object associated with the current exception.
  3. ExceptProc variable - points to the lowest-level RTL exception handler.
Фича в том, что оператор new уже выделил память для экземпляра класса TObject, а тут ррраз! И исключение! Что делает С++? Он тут же освободит память - не надо ставить блок try...catch. Все сделает С++. Ну, как говориться, приятная неожиданность.

(КоТ: это стандарт, и кто не знает его, как может говорить, что знает С++?)

Ну, это только для тех, кто не очень хорошо знает ОР и С++. Впрочем, такие "детские" неожиданности не избавляют программера в С++ от необходимости защиты динамических ресурсов. В ОР же в это время можно попить пива ;)

Я не привожу примеры реализации более полезных УМНЫХ указателей, реализующих сборку мусора и правильную работу с ресурсами вообще.

Судя по всему, автор прочитал книгу Джефа Элджера "C++", испестренную идеями УМНЫХ, ВЕДУЩИХ, ГЕНИАЛЬНЫХ указателей и сборки мусора. Здесь хочу заметить, что я иногда читаю книги с карандашом в руке. Это очень хорошая, умная (КоТ: ведущая и гениальная ;-) книга про С++. Только во время ее чтения, постоянно задумываешься, а как можно сделать тоже самое в ОР. В результате после прочтения книга превратилась в записную книжку, испестренную замечаниями о том, насколько проще и красивее выглядела бы в ОР большая часть предлагаемых решений. Для интересующихся - основная идея в использовании свойств и интерфейсов.

1) Работа с несколькими "собственными" кучами. Например, все покупатели складываются в одну кучу. А поступаемые товары в другую… Как видите осталось только реализовать менеджер кучи, что в рамках С++ вещь простая и ведущая себя незаметно (как встроенная фича). Можно так извернуться в Делфях? Нет

(КоТ: Если можно проще и лучше, так изворачиваться-то нафиг?)

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

2) Помимо "многокучности", оператор new предоставляет вам возможность "виртуального" размещения объекта. Например в файле, в Сети, где вашей душеньке будет угодно. Это тоже недоступно в Делфях.

(КоТ: Это и в С++ без корбы тоже не особенно хорошо получается, кстати. И опять же, нафига? чтобы без спроса прога в своп лазила? Или в инет звонила?)

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

Кстати о преобразовании типов. Делфи обязан безопасному преобразованию типов(as и is) C++, а точнее шаблону dynamic_cast.

(КоТ: Страуструп: "как правило, НЕБЕЗОПАСНО (выделено мной - КоТ) использовать указатель, преобразованный или приведенный с помощью функций …_cast к типу, отличному от типа объекта, на который он указывает.") Да, а в ОР эта вещь абсолютно безопасна…

Не уверен, что кто-то кому-то обязан, тем более шаблону. У ОР всегда была и остается система RTTI, намного превосходящая возможности С++. Да и вообще, RTTI - это обобщенный языковой механизм. При чем здесь конкретные реализации?

Правда в Делфях такое же ограничение на множественное наследование, как и в Яве. Один класс должен быть интерфейсным.

Что за терминология? Дмитрий, наверное, имел в виду, что в списке предков класса ДОЛЖЕН быть указан один класс, и МОЖЕТ быть указано сколько угодно интерфейсов.

Дело в том, что в Делфи тип class реализован через одно очень загадочное место. Связано это с большой нелюбовью паскаля к памяти

(КоТ: это БЫЛО в ДОСе, десять лет назад, но ведь с тех пор воды утекло - !!!)

Можно, конечно, и так сказать. Все в мире относительно. Но я до сих пор встречал очень мало людей, достаточно глубоко знающих устройство классов в Delphi, точнее мне доводилось только читать их труды. И статью Дмитрия тяжело отнести к таким трудам. И почему он решил, что ОР не любит память?

Тип указатель в паскале создан только для того, чтобы указывать на что-то в динамической памяти(куче). Он создан, как шлюз между статической памятью паскаля и кучей. Странно, но зачем-то разработчики языка оставили возможность приводить целое к указателю (КоТ: к дождю, может быть? ;-)

Ни разу не доводилось слышать об ограничении указателей ОР на работу только с кучей. Возможность же приводить целое к указателю позволяет "двигаться" по памяти (не думаю, что это секрет для Дмитрия). Кстати, для указателей на строки допустимы операции "+" и "-" (в том числе в комбинации с целыми) без приведения типов.

Такое понятие как ссылка не знакомо паскалю

(КоТ: тогда в 6.0 под ДОС я работал не с ссылками… а с чем???)

Ссылка в терминологии ОР - это типизированный указатель. А используя термины С++ (КоТ: Страуструп: "Ссылка является альтернативным именем объекта.") ссылкой в ОР являются формальные параметры методов, объявленные с использованием var или out (возможно кто-то не знал, out - то же, что и var, только работает исключительно на возврат значения). Кроме того, чистой воды ссылками являются объектные переменные (Button1: TButton).

Если вы пишите класс "комплексное число", а затем решаете создать массив чисел, то array [1..10] of TComplex; будет на самом деле занимать в памяти 4*10 байт плюс выравнивание. Т.о. вы может быть хотели именно массив ТОЛЬКО КОМПЛЕКСНЫХ чисел, а не указателей на них. Но вместо этого, после инициализации, у вас будет израсходовано (4*10 + 10*sizeof(TComplex)) байт памяти. Короче сами считайте

Действительно, использовать классы ОР в массивах не очень удобно. Есть несколько более экономичных решений:
  • 1) Можно организовать свой менеджмент памяти для TComplex, размещая экземпляры в памяти подряд (например, в заранее выделенном пуле), и работу, скажем, на основе динамического массива. Не самое простое решение, но весьма эффективное и красивое (КоТ: Кстати, активно применяется в С++-модели).
  • 2) Можно вместо классов использовать записи record, организовав их в массив, являющийся свойством по умолчанию какого-то класса:
    TItem = recordend;
    TArray = class
    public
      property Items[Index: integer]: TItem
      read GetItem
        write SetItem;
      default;
    end;
    Это будет самое экономичное решение, т.к. каждый экземпляр любого класса имел бы как минимум ссылку на таблицу VMT. А запись содержит только необходимые данные. Вся же логика работы - в классе TArray.
  • 3) Можно использовать старые "объекты" Паскаля вместо "классов": TComplex = object … end; И массив таких объектов будет содержать сами объекты, а не ссылки на них. Это будет самое оригинальное решение. Кстати, на таких объектах построена библиотека KOL (http://xcl.cjb.net/) - аналог VCL. Размер EXE файлов с использованием этой библиотеки начинается от 4.5К (если не изменяет склероз :)
Одним словом, проблема в разработчике, а не в языке. (КоТ: Вот!!!)

Паскаль маленький язык и это не недостаток. (КоТ: Уф!. Ну сколько можно говорить о паскале 10-летней давности?) ?) Его не замечаешь, когда пишешь прогу большую или маленькую. (КоТ: это высшая похвала паскалю вообще. Лучшая одежда - та, которой не замечаешь.) Почему? (КоТ: Потому, что это хороший язык.)

Потому что Паскаль от Борланд специальный язык, т.е. предназначен для узкой области. Узкая - это не значит, что программ мало, просто цели в этой области отличаются не намного.

(КоТ: "С++ создавался для того, чтобы ИЗБАВИТЬ автора и его друзей ОТ ПРОГРАММИРОВАНИЯ НА АССЕМБЛЕРЕ" - (с) Бьерн Страуструп. Дельфа, возможно, создавалась для того, чтобы избавить автора от программирования на паскале, который вообще создавался изначально для ОБУЧЕНИЯ ПОНЯТИЯМ информатики. Оба эти языка свои цели выполнили с блеском. Ну так о чем же спор, цели-то разные?)

Не знаю, может быть ОР и маленький язык. Однако я оцениваю свои знания ОР не более, чем на 60-70% (хотя меня как-то угораздило сдать экзамен на сертификат Brainbench Certified Master Delphi Programmer :), не включая сюда VCL или среду Delphi, разговор только о самом языке. Если охватить все, то я вообще ничего не знаю. Поэтому мне даже как-то неловко заниматься здесь исправлениями. Я считаю, что для этого необходим куда больший кругозор. Но пока за эту задачу никто не взялся. Видимо настоящим профессионалам просто не до этого.

По поводу узости области применения. До последнего времени я считал, что единственное, что нельзя делать в Delphi - это писать драйвера (ОР тут ни причем, это не языковое ограничение). Но недавно натолкнулся на пример создания в Delphi 3 драйвера VxD. Что еще? Игрушки, сетевые сервисы, системные утилиты, распределенные базы данных, научные программы, средства мультимедиа в Delphi пишут и очень успешно. Так о чем речь?

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

После таких утверждений становится странно, как человек позволяет себе критиковать продукт, о котором имеет лишь зачаточное представление. За что "все остальное" отвечает среда? В виде списка, пожалуйста.

Delphi IDE - это, по большому счету, лишь оболочка, набор зацепок к возможностям библиотеки VCL и шикарный пример использования возможностей языка. Это продукт, тратящий наименьшее количество усилий для выполнения одной и той же работы в сравнении с аналогами. Ведь в лице библиотеки VCL он в design-time использует тот же самый код, который работает в готовом приложении. Для сохранения спроектированной формы со всеми компонентами в ресурс Delphi достаточно одной строчки кода!

И потом, "склеивание" и "написание" компонент - вещи по своей сложности абсолютно разные. Visual Basic тоже хорошо склеивает COM-компоненты, только вот с их написанием у VB как-то не очень… То, что ОР позволяет легко и непринужденно создавать и склеивать любые компоненты говорит лишь о его мощности, продуктивности и универсальности. Совершенно очевидно, что сегодня ОР по этому показателю не имеет не то что конкурентов, а даже толковых аналогов.

VCL не является языковым расширением Паскаля - это "ОО" библиотека. Транспортом же между такими библиотеками и отдельными компонентами выступает некая переделка СОМ от Борланд.

Да, VCL - вещь самостоятельная, пока она строится на ОР. Далее автор, похоже, говорит о RTTI. Но причем здесь транспорт между библиотеками и отдельными компонентами? Библиотека - понятие чисто условное. Каждый написанный мной компонент становится полноправной частью VCL. Правильнее, наверно, говорить о транспорте между компонентами и их пользователями, в частности средой Delphi IDE.

К вопросу о переделках. Delphi начинала разрабатываться где-то в 92-93 году. Трудно говорить, кто кого переделал. Да это и не важно. Важно то, что компонент Delphi в полной мере обладает обоими механизмами.

И опять же отбросьте этот транспорт, который не является частью языка, и от Делфи ничего не останется. (КоТ: Отбрось Gdb\Gtk, STL - что останется от милого нашему сердцу С++?) Поэтому Делфи очень гармоничная со своими недостатками среда для разработки GUI приложений под винды.

Да, RTTI - незаметная, но ключевая для Delphi технология. А вот с тем, что она не является частью языка можно крепко поспорить. Достаточно вспомнить операторы AS и IS, которые целиком базируются на RTTI. Да и от TObject никуда не убежишь. Попробуй скажи, что это не часть языка. А ведь основное содержание TObject - реализация RTTI.

Кроме того, Delphi идеально подходит не только для создания GUI приложений, но и консольных, и приложений без визуального интерфейса вообще (например, сервисы Windows NT). К счастью компоненты Delphi не ставят во главу угла визуальность/невизуальность. Это абсолютно универсальные в применении классы. Уже поэтому Delphi разительно отличается от, например, MSVC++, где в основе слова "Visual" лежит наличие у компонента оконного идентификатора и множество маловразумительных макросов и комментариев по тексту, которые нельзя (!) редактировать. Вот где действительно безопасный язык! Ведь программист может все испортить :)

По поводу недостатков можно сказать лишь то, что вряд ли у конкурирующих с Delphi продуктов их меньше. А вообще, давайте взглянем на Delphi 6 и Kylix. Уверен, что сюрпризов там будет более чем достаточно.

(Кот: я очень надеюсь, что у нас научатся, наконец, считать "Итого", а не только недостатки и достоинства отдельно).

Похоже, Дмитрий применительно к безопасности программирования рассматривал только те моменты, которые, по его мнению, хорошо смотрелись в С++ в сравнении с ОР. Здесь, кстати, стоит упомянуть такие преимущества С++ над ОР, как возможность понижать видимость членов класса, а также указание const при объявлении метода, что гарантирует неизменность атрибутов объекта при вызове метода.

Однако не стоит забывать, что ОР является языком, который действительно ставит во главу угла безопасность практически во всем. Можно долго перечислять все его тонкости, избавляющие программера от головной боли и рутиной работы. Как пример, можно привести директиву implements для свойств (делегирование реализации) или объявление глобальных переменных в разделе threadvar для поддержки многопоточности, или замечательную реализацию работы со строками и динамическими массивами на уровне компилятора. Очень важное для безопасности программирования свойство - объявление новых типов.

Гради Буч: "К сожалению, конструкция typedef не определяет нового типа данных и не обеспечивает его защиты. Например, следующее описание в С++:
typedef int Count;
просто вводит синоним для примитивного типа int."

В ОР же мы можем создать абсолютно новый тип. Для этого надо применить ключевое слово type:
type
  Count = type int64; // другой тип
  Alias = int64; // синоним
Типы Count и int64 уже не будут совместимы без приведения типов.

А вот пример стандартизованной фичи компилятора (соответственно и языка) С++:
long FileSize = 256 * 1024;

В 16-битном компиляторе вы в результате получите 0. Очень приятный сюрприз! А дело в том, что 256 и 1024 по отдельности попадают в int (2 байта), а их произведение уже в long (4 байта). Однако стандарт С++ как раз в том и заключается, что произведение будет также помещено в int. И уже только после этого произойдет присвоение к long. Соответственно туда попадает только младшая часть произведения, которая равна нулю. Спасает написание в форме 256 * 1024L. На 32 битах все будет нормально, т.к. размеры типов int и long совпадают (4 байта).

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

(КоТ: плохо выглядит).

А как "эстетичны" в каждом header"е С++ конструкции типа:
#ifndef _MYHEADER_H
#define _MYHEADER_H
… body of the header …
#endif
Получается, я вместо компилятора должен следить, чтобы в проекте оказалась только одна копия каждого файла.

Кстати, особенности ОР как языка обеспечивают не только безопасность программирования, но и безопасность полученного софта, как таковую. К примеру, более половины дыр в безопасности программ отраженных в Bugtraq возникают из-за проблемы переполнения буфера. А эта проблема является визитной карточкой С/С++. Дошло до того, что выпускаются специальные пакеты, которые патчят исходники С++. Как один из вариантов решения проблем безопасности предлагают писать на Pascal…

После всего, что я тут наговорил, может возникнуть мысль: "А почему же тогда Борланд двигает Делфи?". "И почему VCL написан на паскале, а не на С++?". Резонно. Мыслям вообще свойственно появляться в головах человеков.

Нет, мысль возникает не такая. С этим все ясно и так, VCL и Delphi не могут быть написаны ни на чем другом (можно, конечно, на С++ написать компилятор ОР, что вполне реализуемо, и потом в нем все делать, но ведь разработчика такой способ явно не устроит).

Возникает другая мысль. Почему уровень знаний ОР у очень многих программистов так удручающе низок (КоТ: С С++ ситуация ничуть не лучше. Груда книг всяких, прости господи, пересмешников. А если прочитать 1 (один) раз Страуструпа, множество вопросов просто отпадет). Понятно, что литература у нас в основном "для чайников". Но иногда надо хотя бы help читать. Похоже считается делом чести начитаться умных книжек "с примерами приложений на С++", а для Delphi, мол, можно ограничиться знанием Object Inspector"а.

При этом многие такие программисты почему-то считают для себя возможным критиковать возможности ОР. Может быть потому, что Delphi дала им возможность быстро и легко воплотить свои идеи? А потом вдруг что-то не получилось… И вот, виновата Delphi. Можно с уверенностью сказать, что С++ такому программисту все равно не поможет.

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

(КоТ: за что я вообще и выбрал линух - это система людей, имеющих роскошь на рынок в некоторых местах вообще плевать. Хотя от рынка, конечно уйти нельзя. Да и зачем? В умеренных дозах рынок - это очень хорошо.)

Для любящих спорить. Не стоит критиковать какие-то возможности продукта, не до конца в них разобравшись. Современные языки слишком многогранны, чтобы один человек досконально знал хотя бы два языка. Я лично не уверен, что все мои рассуждения на 100% достоверны, но старался, как мог. Поэтому буду рад техническим исправлениям.

"Портос, если Вы говорите глупости, то делайте это, пожалуйста, только от своего имени"

P.S. Красота драгоценного камня, как известно, зависит не только от породы, но и от мастерства огранщика. Только тогда обычный белый свет превращается в нем в причудливую игру разноцветных искр. Так что учите матчасть, и Delphi вас не подведет :)

(КоТ: Два слова напоследок - не удержался. Я полагаю, что С++, что дельфа - языки одного уровня, но разных подуровней. Бессмысленно их сравнивать вообще. С++ старше - хотя бы поэтому дельфа лучше, т.к написана на его крови, если можно так сказать.
Но дельфа все-таки следующее поколение языков. Стоит ли сравнивать сына с отцом, и чему удивляться? Уже народ типами не оперирует, уже оперирует свойствами и объектами. Кто даст хороший язык для этого, тот и выиграет.
А что до С++ - учите матчасть… И не хуже будет, чем в дельфе. ;)

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

Обсудить статью на форуме


Если Вас заинтересовала или понравилась информация по разработке на Delph - "Игра отражений", Вы можете поставить закладку в социальной сети или в своём блоге на данную страницу:

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


Copyright © 2008 - 2024 Дискета.info