きわめて誰得なエントリですが。
「C++では初期化は必ず『基底クラス→派生クラス』の順に行われる」と昨日のエントリで書きましたが、そうでない言語もあるという話。
有名なのがDelphi
Delphiでは、先に派生クラスのコンストラクタが呼び出されます。加えて基底クラスのコンストラクタは自動的には呼び出されません。派生クラスのコンストラクタから明示的に呼び出す必要があります。
program CallingVirtualMethodFromConstructorOfBaseClass; type TBase = class public constructor Create; destructor Destroy; override; procedure DoSomething; virtual; end; TDerived = class(TBase) public constructor Create; destructor Destroy; override; procedure DoSomething; override; end; constructor TBase.Create; begin Writeln('constructiong TBase'); DoSomething; { 仮想関数を呼び出す } end; destructor TBase.Destroy; begin Writeln('destructing TBase'); end; procedure TBase.DoSomething; begin Writeln('TBase.DoSomething'); end; constructor TDerived.Create; begin inherited Create; { 基底クラスのコンストラクタを呼び出す } Writeln('constructiong TDerived'); end; destructor TDerived.Destroy; begin Writeln('destructing TDerived'); inherited Destroy; { 基底クラスのデストラクタを呼び出す } end; { TBase.Createから呼び出される } { TDerivedから直接呼ばれていないことに注意 } procedure TDerived.DoSomething; begin Writeln('TDerived.DoSomething'); end; var Base: TBase; begin Base := TBase.Create; Base.Free; Writeln; Base := TDerived.Create; Base.Free; end.
すでに手元にDelphiはないので、いつものようにFreePascalをDelphiモードにしてコンパイル。こんな感じで。
$ fpc -Mdelphi CallingVirtualMethodFromConstructorOfBaseClass.pas
実行結果。
constructiong TBase TBase.DoSomething destructing TBase constructiong TBase TDerived.DoSomething constructiong TDerived destructing TDerived destructing TBase
仮想関数DoSomething
が基底クラスのコンストラクタから呼ばれているのがわかります。
見てのとおり基底クラスのコンストラクタ、デストラクタが呼び出されるタイミングは派生クラスでのコードの書き方で決まります。ですから、こんなふうに書き直すと…
constructor TDerived.Create; begin Writeln('constructiong TDerived'); inherited Create; { 基底クラスのコンストラクタを呼び出す } end; destructor TDerived.Destroy; begin inherited Destroy; { 基底クラスのデストラクタを呼び出す } Writeln('destructing TDerived'); end;
…こういう結果になります。
constructiong TBase TBase.DoSomething destructing TBase constructiong TDerived constructiong TBase TDerived.DoSomething destructing TBase destructing TDerived
さらにこうすると…
constructor TDerived.Create; begin { inherited Create; } { 基底クラスのコンストラクタの呼び出しをコメントアウト } Writeln('constructiong TDerived'); end; destructor TDerived.Destroy; begin Writeln('destructing TDerived'); { inherited Destroy; } { 基底クラスのデストラクタの呼び出しをコメントアウト } end;
…こうなります。
constructiong TBase TBase.DoSomething destructing TBase constructiong TDerived destructing TDerived
DoSomething
メソッドは基底クラスのコンストラクタから呼び出されているので当然TDerived.DoSomething
は呼ばれません。
どうしてこうなっているのか
Delphiのマニュアルがどっかにあったはずと書棚を引っ掻き回したところ、奥の方に「Inside Delphi (Borland programming series)」があるのを発見したのでそこから引用。
コンストラクタと仮想メソッドはC++プログラミングではおなじみのものですが、DelphiとC++ではその実装の仕方が違います。DelphiとC++の重大な違いは、コンストラクタが実行されている間の仮想メソッドへのアクセスの仕方にあります。Delphiでは、コンストラクタが呼び出される前にオブジェクトの型が決められ、コンストラクタと継承されたコンストラクタの実行中はその型は不変です。一方C++では、オブジェクトの型はそのほとんどの派生クラスのコンストラクタが実行されるまで決まりません。C++では自動的に基本クラスのコンストラクタが最初に呼ばれ、各基本クラスのコンストラクタが呼び出されている間は、オブジェクトの型は基本クラスの型になります。
この違いは、コンポーネントから仮想メソッドを呼ぶ場合に重要になります。C++では、実際に呼ばれるメソッドは、基本クラスのメソッドですが、Delphiでは、実際のメソッドは派生クラスのメソッドになります。
(中略)
Delphiはプログラマに対して継承したコンストラクタとデストラクタの呼び出しを要求しますが、どの時点で呼び出すかの決定はプログラマに任せられています。本書の中でも、この決定権を利用した例がいくつかあります。熟達したC++プログラマの中には、C++に較べてDelphi Pascalがより大きな柔軟性を提供する一方で、安全性を低下させていることに不安を覚える人がいるかも知れません。しかし、Delphiを少しでも使い始めると、Delphiのこの特徴を享受するようになるでしょう。
享受できたのか、すでに忘れてしまいましたが…。
「Delphiでは/その型は不変」「C++では/型は基本クラスの型」のところをコードで表現してみると、こんな感じ。
C++のばあい。
#include <iostream> #include <typeinfo> class Base { public: Base() { std::cout << "Base: address = " << this << " / type = " << typeid(*this).name() << std::endl; } }; class Derived : public Base { public: Derived() { std::cout << "Derived: address = " << this << " / type = " << typeid(*this).name() << std::endl; } }; int main(int, char* []) { Derived d; return 0; }
実行結果。
Base: address = 0xbffff318 / type = 4Base Derived: address = 0xbffff318 / type = 7Derived
アドレスは同じですが、基底クラスのコンストラクタの中ではthis
は基底クラスのオブジェクトになっています。
Delphiのばあい。
program ObjectType; uses SysUtils; type TBase = class public constructor Create; end; TDerived = class(TBase) constructor Create; end; constructor TBase.Create; begin Writeln('TBase: address = ', IntToHex(Integer(Self), 8), ' / type = ', Self.ClassName); end; constructor TDerived.Create; begin inherited Create; Writeln('TDerived: address = ', IntToHex(Integer(Self), 8), ' / type = ', Self.ClassName); end; var Base: TBase; begin Base := TDerived.Create; Base.Free; end.
実行結果。
TBase: address = 000BB034 / type = TDerived TDerived: address = 000BB034 / type = TDerived
先に派生クラスのコンストラクタが呼ばれているので当然の結果ですが、基底クラスの中でもSelf
(C++のthis
に相当)は派生クラスのオブジェクトとして扱われます。
今回の結論
たどたどしいながらいまだにDelphiのコードを書けたことに自分でびっくりした。
Inside Delphi (Borland programming series)
- 作者: Ray Lischner,光田秀
- 出版社/メーカー: アスキー
- 発売日: 1997/12
- メディア: 単行本
- クリック: 2回
- この商品を含むブログ (1件) を見る