エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

派生クラスのコンストラクタが実行される前に派生クラスを操作する・Delphiのばあい

きわめて誰得なエントリですが。


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++プログラミングではおなじみのものですが、DelphiC++ではその実装の仕方が違います。DelphiC++の重大な違いは、コンストラクタが実行されている間の仮想メソッドへのアクセスの仕方にあります。Delphiでは、コンストラクタが呼び出される前にオブジェクトの型が決められ、コンストラクタと継承されたコンストラクタの実行中はその型は不変です。一方C++では、オブジェクトの型はそのほとんどの派生クラスのコンストラクタが実行されるまで決まりません。C++では自動的に基本クラスのコンストラクタが最初に呼ばれ、各基本クラスのコンストラクタが呼び出されている間は、オブジェクトの型は基本クラスの型になります。
この違いは、コンポーネントから仮想メソッドを呼ぶ場合に重要になります。C++では、実際に呼ばれるメソッドは、基本クラスのメソッドですが、Delphiでは、実際のメソッドは派生クラスのメソッドになります。
(中略)
Delphiプログラマに対して継承したコンストラクタとデストラクタの呼び出しを要求しますが、どの時点で呼び出すかの決定はプログラマに任せられています。本書の中でも、この決定権を利用した例がいくつかあります。熟達したC++プログラマの中には、C++に較べてDelphi Pascalがより大きな柔軟性を提供する一方で、安全性を低下させていることに不安を覚える人がいるかも知れません。しかし、Delphiを少しでも使い始めると、Delphiのこの特徴を享受するようになるでしょう。


Inside Delphi (Borland programming series)」p.53


享受できたのか、すでに忘れてしまいましたが…。
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


先に派生クラスのコンストラクタが呼ばれているので当然の結果ですが、基底クラスの中でもSelfC++thisに相当)は派生クラスのオブジェクトとして扱われます。

今回の結論

たどたどしいながらいまだにDelphiのコードを書けたことに自分でびっくりした。


Inside Delphi (Borland programming series)

Inside Delphi (Borland programming series)