The Delphi Bug List

Entry No.
416
コンパイラ - コード生成
コンパイラは、型付き定数の(古いスタイルの)オブジェクトを正しく初期化しない
1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02 4.03 5.0 5.01 6.0 6.01 6.02 Kylix 1.0
Absent Exists Exists Exists Exists Exists Exists Exists Exists Exists Exists Exists Exists Exists Exists
解説
Reported by Antoine Leca; checked by Duncan Murdoch, Reinier Sterkenburg

型付定数として生成された古いスタイルのオブジェクトを指す継承ポインタを通して仮想メソッドを呼び出したとき、Delphi 2/3/4 は正しいメソッドを選択しません。それは二つだけ足りない、つまり、正しいメソッドよりも二つ前のメソッドを選択してしまいます。実際のところこれは、まるで "object" が "class" と同じ VMT を持っている(これは誤りです)かのようです。

object はコンストラクタを持つ必要がありません(BP 7 のマニュアルを参照)。仮想メソッドを持つオブジェクトが初期化されるにはコンストラクタが呼ばれなければならないことは、この言語の規則です。しかしながらBP 7のコンパイラはそれ(仮想メソッドを持つobjectを、コンストラクタを呼ばずに使用する事)を許可しており、なおかつコンストラクタの呼び出しを行わない定数オブジェクトの宣言も許しています(これは文書化された振る舞いです)。しかしDelphi の 32 ビットコンパイラはそれほど寛容ではありません。

これはコンパイラのバグです。なぜなら、ドキュメント化はされていませんが、古いスタイルのオブジェクトを扱うための機能は全て提供されているからです。残念ながらコンパイラは、型付定数オブジェクトの VMT フィールドを間違った値で初期化してしまうのです。

以下のソースファイルはこの問題を確認するためのものです。これは BP7 と Delphi 1.02 ではうまく動作します。その出力は次のようになります:

        TObj.c elem=1
        TObj.c elem=1

しかし、Delphi 2.01では失敗し、出力は次のようになります:

        TObj.c elem=1
        TObj2.a elem=1

Delphi 3.01,4.02,5.01,6.02では、2行目の実行時に Access Violation が生成されます。
Kylix(1.0)では、"Program Test416 received signal SIGSEV(11).Process stopped. Use Step or Run to continue." が発生します。

ソース:

{$ifdef WINDOWS}
uses WinCRT;
{$endif}

type TObj=object
  elem:Byte;
  procedure a;virtual;
  procedure b;virtual;
  procedure c;virtual;
end;

type 
  PObj=^TObj;

type 
  TObj2=object(TObj)
    procedure a; virtual;
  end;

procedure TObj.a;  
begin 
  WriteLn('TObj.a elem=',elem); 
end;
procedure TObj.b;  
begin 
  WriteLn('TObj.b elem=',elem); 
end;
procedure TObj.c;  
begin 
  WriteLn('TObj.c elem=',elem); 
end;

procedure TObj2.a;  
begin 
  WriteLn('TObj2.a elem=',elem); 
end;

const 
  Obj:TObj2 = (elem:1);
const 
  Ptr:PObj = @Obj;

begin
  Obj.c;
  Ptr^.c;
end.
解決策 / 回避方法

object 型にもコンストラクタを追加し、初期化部で(仮想メソッドが呼ばれる前に)それを呼び出します。コンストラクタ内で呼ばれるコード(_ObjSetup)が、そのオブジェクトの VMT フィールドに新しい値(オフセット +8)を格納します。これこそが仮想メソッドを呼ぶ際に期待される値です。

上のコードは次のようになります(このコードは BP7,Delphi 1.02,2.0x,3.02 で正しく動作します):

type TObj=object
  elem: Byte;
  constructor init;
  procedure a; virtual;
  procedure b; virtual;
  procedure c;virtual;
end;

type 
  PObj=^TObj;

type 
  TObj2=object(TObj)
    constructor init;
    procedure a;virtual;
  end;

constructor TObj.init;  
begin 
end;

procedure TObj.a;  
begin 
  WriteLn('TObj.a elem=',elem); 
end;
procedure TObj.b;  
begin 
  WriteLn('TObj.b elem=',elem); 
end;
procedure TObj.c;  
begin 
  WriteLn('TObj.c elem=',elem); 
end;

constructor TObj2.init;  
begin 
  inherited init 
end;
procedure TObj2.a;  
begin 
  WriteLn('TObj2.a elem=',elem); 
end;

const 
  Obj:TObj2 = (elem:1);
const 
  Ptr:PObj = @Obj;

begin
  Obj.init;
  Obj.c;
  Ptr^.c;
end.
Latest update of this entry: 2002-04-03
本家 The Delphi Bug List のエントリーはこちら
The Delphi Bug List 日本語訳 へ