Swanman's Horizon

性欲をもてあましつつなんらかの話をするよ。

Delphiプログラマを見分ける10の質問(解説編)。

はじめに

先日公開した10の質問、いかがだったでしょうか。「この質問を作ったのは誰だあっ!」と海原雄山が怒鳴り込んできそうなくらいDelphiを扱う上では全く必要ない知識ばかりでしたが、知っていれば明日のDelphiトークのひとつとして使える程度には役立つかもしれません。
なお、基本的にDelphi 10.1 Berlin上で確認していますが、他のバージョンのコンパイラが同じ挙動を示すかは不明です。

1.「const」と「var、out」の違いを参照という観点でひとつ挙げよ。またその違いを無くすためにはどうすれば良いか説明せよ。

「var、out」は必ず参照渡しになるが、「const」は必ずしも参照渡しにはならない。Ref属性を使うことで参照渡しを強制できる。

「const」と「var、out」の違いは上述の参照渡しの件以外に「書き換えが可能か否か」というものもあり、constで渡されたものが必ず参照渡しになっていれば引数のアドレスを逆参照することで書き換えが可能になります。しかしすでに書いたとおり参照渡しにならず値渡しになる時もあるため、その場合は逆参照して書き換えても引数として渡した元の変数は変わりません。

constが値渡しになる条件は明記されていませんが、基本的にはレジスタに乗るか乗らないかということがひとつの条件になります。つまり32bit環境では4バイトまでの変数が、64bit環境では8バイトまでの変数が値渡しになります。

Ref属性はXE4で新しく導入された属性で、関数パラメータに付けることで参照渡しを強制します。例えば以下のように使用します。

function Foo(const [Ref] Value: Integer): Pointer;
begin
  Result := @Value;
end;

このRef属性はdocwikiの説明を見る限りconst用に実装されたみたいですが、constが付いていなくても動作します。例えば以下のように指定すると、あたかもvarとして渡されたかのように振る舞います。

procedure Bar([Ref] Value: Integer);
begin
  Value := Value + 1;
end;

これは、

procedure Bar(var Value: Integer);
begin
  Value := Value + 1;
end;

と等価です。

また、constに関する面白い挙動として、プロパティを渡した時のものがあります。以下のようにフィールドが指定されたプロパティと、メソッドが指定されたプロパティでは動作が異なります。

type
  TFoo = class
  private
    FField1: Integer;
    function GetField2: Integer;
  public
    property Field1: Integer read FField1;
    property Field2: Integer read GetField2;
  end;

このようなプロパティをそれぞれ上述のFoo関数に渡すと、Field2はコピーの参照が渡される(実質値渡し)のに対して、Field1の時はFField1変数の参照が渡されます。これを利用すると、RTTIやclass helperを用いることなく、手軽にreadonlyなプロパティを書き換えるハックとして使用可能です。

2.文字列や動的配列などの型は自動で初期化されるため自分でnil等を代入する必要がないが、初めての使用時にnilや空文字列で初期化されていない場合があるのはどんな時か?またその理由を説明せよ。

関数の戻り値として文字列や動的配列が含まれるrecordを使用している時。実態としては代入先変数が暗黙の引数として参照渡しされるため、代入先が使用済みだとその値が渡されてしまう。

文字列、動的配列、インターフェース、およびバリアントはコンパイラによって自動管理され、使用時には自動的に初期化されます。これは明示的に初期化しないと不定値となるローカル変数の場合も同様で、変数の中にゴミが入ったままだと参照カウンタが正常に動作しないため、必ず初期化されます。

それでは何故戻り値の型がrecordだと初期化されないかというと、戻り値の型がレジスタサイズに収まる場合はレジスタで、そうではない場合は暗黙の引数を参照渡しして返すようになっているためです。
例えば以下のような関数があったとします。

function Foo: TBar;
begin
  Result.Text := 'ABCDEF';
  ...
end;

これは以下のような手続きと等価で、実際にこのように変換して呼び出されます。

procedure Foo(var Result: TBar);
begin
  Result.Text := 'ABCDEF';
  ...
end;

このため、Foo関数に渡す引数がすでに使用済みだと、Foo関数には何らかの値が入った状態で「Resultが渡される」という状況が発生します。特殊なケースではありますが、自動管理される型は必ず初期化されていると思っていると、以下のような処理でバグが発生する恐れがあります。

function Foo(const Values: array of Integer): TBar;
var
  value: Integer;
begin
  for value in Values do
    Result.Text := Result.Text + value.ToString; // <= Result.Textは代入済みの可能性があり、その場合文字列の先頭にゴミが付く
end;

初期化していないResult.Textをいきなり参照している時点でおかしいとツッコミを受けそうですが、上述の通り文字列は自動で初期化される型のひとつであり、これらの型は変数宣言後に何も代入しないまま参照しても文法的に違法ではありません。

3.複数の文字列変数を連結するとき、「sA := s1 + s2; sB := s3 + s4; s := sA + sB;」と「s := s1 + s2 + s3 + s4;」は足し算の数だけ見れば等価だが、後者の方が良いのは何故か。

文字列の+演算子での連結は、連結する個数がいくつであろうと1つの文につき1つの関数が呼び出されるため。

これは単純な話で、それぞれを実際の関数呼び出しに置き換えると以下のようになります。

begin
  // sA := s1 + s2; sB := s3 + s4; s := sA + sB;
  UStrCat3(sA, s1, s2);
  UStrCat3(sB, s3, s4);
  UStrCat3(s, sA, sB);

  // s := s1 + s2 + s3 + s4;
  UStrCatN(s, 4, s1, s2, s3, s4);

このように連結と代入を複数の文に分けた場合、関数呼び出しもその分増えることになるため、関数内で行われるメモリの再確保やコピーもその度に行われることを考えれば、一度に行う後者の方がより良いと考えられます。

余談として、Concat関数という文字列や動的配列の連結用関数があります。この関数をdocwikiで調べると「プラス演算子の方が Concat より高速です」と以前から書いてあるんですが、今のコンパイラでは上記コードと同じように2つの連結であればUStrCat3関数に、3つ以上の連結であればUStrCatN関数にそれぞれ置き換えられます。つまり現在では両者は等価です。

4.nilが代入されているインスタンスの(クラスメソッドではない)メソッドを呼び出そうとした場合でも読み取り違反などのエラーが発生しないのは主にどんな状況か?またその理由を説明せよ。

自身が静的メソッドであり、インスタンスのフィールドに一切触らない場合。nilが入っているSelfには一切アクセスしないため。

以前書いた記事でも触れましたが、メソッドの呼び出しは実際には手続き/関数のそれと同等であり、変換して書くと以下のようになります。

procedure TFoo.Bar(Value: Integer);
begin
  FBaz := Value;
end;
// ↑これと↓これは等価
procedure Bar(Self: TFoo; Value: Integer);
begin
  Self.FBaz := Value;
end;

ここで下の手続き版を基準に考えると、Selfにnilが入っていれば代入が失敗することは容易に想像できます。クラスとは大雑把に言えばレコードのポインタ参照のような物であり、実体のないレコードに代入はできないからです。逆に考えれば、Selfに触りさえしなければエラーは発生しません。

メソッド内の処理でエラーが発生しない条件は分かりましたが、メソッドを呼び出した時点でエラーが発生する場合もあります。仮想メソッド(virtual)、あるいは動的メソッド(dynamic)を使用している場合です。
仮想メソッドの呼び出しは「仮想メソッドテーブル(VMT)」と呼ばれる暗黙のインスタンス変数を通じて行われます。VMTの中身は継承元を含めvirtualが指定されたメソッド全てのメソッドアドレスを並べた配列のようなものです。VMTはコンストラクタの呼び出し時に暗黙的に初期化されるため、nilが入っているようなインスタンスでは中身を読み出すことはできず、従って仮想メソッドの呼び出しは失敗します。
動的メソッドの呼び出しは「動的メソッドテーブル(DMT)」と呼ばれるVMTのマイナス領域にあるポインタを通じて行われます。DMTはVMTと違って継承元のメソッドは含まれず、自身が実装したメソッドのみテーブル内に存在します*1。呼び出し処理も他と違い、自身が実装していないメソッドは親クラスのDMTを探しに行く必要があるため、CallDynaInstという関数にVMTと動的メソッドインデックスを渡して代わりに呼び出してもらいます。しかしnilが入っているインスタンスの場合、渡すべきVMTも初期化されていないためDMTすら特定できず、動的メソッドの場合も呼び出しは失敗します。
以上を総合すると、virtualでもdynamicでもない静的メソッドであればエラーは発生しません。

5.関数内関数をコールバックを必要とする関数の引数として渡そうとすると「ローカル手続き/関数を手続き変数に代入しました」というエラーが発生してコンパイルできないが、これを回避するにはどうすれば良いか。またその際気を付けることは何か。

「@Func」のように関数ポインタとして渡せば通る。親関数内のローカル変数にアクセスしないようにする。

@演算子は型無しのポインタ(Pointer型)を生成します。そして型無しのポインタは全てのポインタ型に対して代入可能な互換性を持ちます。一方、コールバックとして指定されている引数の型は手続き型や関数型と呼ばれたりもしますが、大きく分けると型付きポインタのひとつです。
関数内関数をコールバックとして渡そうとすると質問文のようにエラーが発生しますが、これを@演算子を用いて単なる型無しポインタとして認識させることで、コンパイラは「コールバックに関数内関数を渡した」のではなく「コールバックにポインタを渡した」と解釈し、コンパイルを通してくれるようになります。

その場限りでしか使わないコールバック関数の実装であれば、この手法はローカル内で記述が完結できるという利点を提供しますが、一方で注意が必要な点もあります。それは親関数のローカル変数に触らないということです。
関数内関数から親関数内のローカル変数を変更することは通常の使用であれば問題ありません。問題はコンパイラが親関数内ローカル変数へのアクセスを「関数内関数が親関数から呼び出された」としてコードを生成することにあります。当然ながらコールバックとして関数内関数を渡した時、その関数内関数を呼び出す元となるのは親関数ではなくコールバックを実装した関数になります。親関数から呼び出された場合は親関数内のローカル変数を触るための情報を関数内関数が得ることができますが、それ以外の場合は情報がないため間違った情報を元にアクセスすることになり、多くの場合エラーが発生します(発生しない場合でも無効な値を読み書きすることになりバグの元になります)。そのため、関数内関数をコールバック関数として渡す場合は親関数に依存しない、自身だけで完結する処理内容にする必要があります。

ちなみに、この方法は「ポインタ型のチェック」がオフの場合(デフォルトはオフ)のみ使用できます。オフの時は関数ポインタに限らずすべての@演算子によるポインタは型無しポインタ(Pointer)になるのであらゆるポインタ型への代入互換性がありますが、オンにして@演算子で型付きポインタが生成されるようになると、当然ながら型が違って代入できないため使用できません。

6.TComponent.FOwnerをはじめ、10 SeattleではWeak属性が指定されていたフィールドが10.1 BerlinではUnsafe属性に置き換えられているが、これはどういった理由が考えられるか。

Weak属性を指定したフィールドは参照先が解放されたときにnilが代入されるようになっており、それが必要無い場合は単純に処理コスト増のデメリットを受けてしまうため。

ARC未実装の処理系ではインターフェースが、ARC実装済みの処理系ではインターフェースとクラスが参照カウントを用いて自動管理されますが、参照カウントの欠点として循環参照が発生し得ることがあります。
循環参照というのは、つまりクラスAがクラスBを、クラスBがクラスAを参照している状態のことで、この場合両者がどこからも参照されていなくても、お互いがお互いを参照し合っているため参照カウントがゼロにならず、自動解放がされなくなりメモリリークが発生します。
これを解決するため、通常の「強い参照」に対して参照カウントを増減しない「弱い参照」がWeak属性です。前述の例で言えば、クラスAがクラスBを強い参照で持ち、クラスBがクラスAを弱い参照で持っている場合、クラスAはクラスBの所有権を持った状態ですが、クラスBはクラスAを見ているだけのような状態であり、クラスAは自身の参照がなくなると自身が持っているクラスBの参照もゼロにして両者を解放します。

ではUnsafe属性というのは何なのかというと、Weak属性からある処理を除いたものになります。そのある処理というのは、強い参照がどこかで解放された場合、同じインスタンスへの弱い参照を持つ変数全てにnilを代入して回るという処理です。この処理を実現するため、Weak属性の指定された変数への代入は単なる代入処理では終わらず、自身の変数アドレスを共通の弱い参照リストに登録する処理が含まれます。また、Weak変数が解放されないままその変数を持つインスタンスの方が解放された場合、弱い参照リストを見てnilを代入されては困るので、弱い参照リストから自身を削除する処理が一緒に走るようになります。説明が下手なので文章としてみると分かりにくいかもしれませんが、TComponentの子管理と似た仕組みと言えば分かる人は分かるかもしれません。

まとめると、Unsafe属性というのは参照カウントも何もかも無視して、単なるポインタと同然に扱う処理と言えそうです。代入や解放時にいちいち登録や登録解除処理が走るWeakに比べれば単なる値の代入で済むUnsafeは速度的にメリットがあるため、これがWeakからUnsafeに置き換えられた理由だと考えられます(※実装者の意図は分からないため、あくまでも推測です)。

7.recordでインターフェース(例えばIInterface)を実装する方法を簡単に説明せよ。

インターフェースで宣言されているメソッドを手続き/関数、あるいはrecordの静的メソッドとして実装し、そのアドレスをフィールドとして持ったrecordを用意し、そのポインタをIInterfaceなどにキャストする。

これはちょっと質問が悪かったと思います。「record『も』使ってインターフェースを実装せよ」の方が正確かもしれません。
インターフェースとは、分かりやすく言えば「どういう名前でどういう引数を持ったメソッドがあるか」ということを並べた定義であり、実態はそれらの関数ポインタを並べたテーブルです。実装コードは含まないため、通常はクラスを使って実装します。というか通常はクラスでしか実装できません。

ではどうやってrecordで実装するかというと、上述の「関数ポインタを並べたテーブル」を自分で作り出す、ということになります。

type
  TIInterface = record
    QueryInterface: function(Self: Pointer; const IID: TGUID; out Obj): HResult; stdcall;
    _AddRef: function(Self: Pointer): Integer; stdcall;
    _Release: function(Self: Pointer): Integer; stdcall;
  end;
  PIInterface = ^TIInterface;
  PPIInterface = ^PIInterface;

function NopQueryInterface(Self: Pointer; const IID: TGUID; out Obj): HResult; stdcall;
begin
  Result := E_NOINTERFACE;
end;

function NopAddRef(Self: Pointer): Integer; stdcall;
begin
  Result := -1;
end;

function NopRelease(Self: Pointer): Integer; stdcall;
begin
  Result := -1;
end;

var
  rec: TIInterface;
  prec: PIInterface;
  intf: IInterface;
begin
  // テーブルを作る
  @rec.QueryInterface := @NopQueryInterface;
  @rec._AddRef := @NopAddRef;
  @rec._Release := @NopRelease;
  prec := @rec;

  // インターフェースにキャストすればそのまま使用可能
  intf := IInterface(@recP);
  intf._AddRef;
  intf._Release;
end;

これを逆に応用すると、インターフェースからレコードにキャストすることでインターフェースのメソッドアドレスを取り出すことや、メソッドアドレス自体を書き換えることも可能になります。

var
  intf: IInterface;
  pprec: PPIInterface absolute intf;
  oldProtect: DWORD;
begin
  intf := TInterfacedObject.Create;
  intf._AddRef; // <= TInterfacedObject._AddRefが呼ばれる
  VirtualProtect(@pprec^^, SizeOf(TIInterface), PAGE_EXECUTE_READWRITE, oldProtect); // <= Delphiのインターフェーステーブルは読み取り専用のコード領域にあるため書き換え属性を付与
  @pprec^^._AddRef := @NopAddRef;
  intf._AddRef; // <= NopAddRefが呼ばれる
end;

なお、今回はインターフェース→レコードへのキャストを行うため各メソッドを関数ポインタとして定義していますが、レコードでインターフェースを実装するという部分だけであれば関数ポインタは必要無く、Pointer型で十分です。さらに言えばレコードすら不要で単なるポインタの静的配列で良く、実際に同様の手法でインターフェースを実装しているSystem.Generics.Defaults内では配列が使用されています。

8.通常inlineが指定された関数・メソッドは処理内容がインライン展開可能な条件であれば呼び出しはインライン展開されるが、呼び出し方によってインライン展開される場合とされない場合が発生するのはどのような状況か述べよ。複数あればなお良い。

他ユニットの外部シンボルを使っていてそのユニットをusesしていない場合、インターフェースから呼び出す場合、whileやrepeatの条件式で使用する場合、コールバック関数として渡す場合(関数アドレスが必要になる場合)など。

関数のインライン展開を軽く説明すると、あまり大きくないサイズの関数呼び出しを関数の中身で丸ごと置き換えてしまう機能です。例えば以下のような関数Fooとその呼び出しがあるとします。

function Foo(Value: Integer): Integer; inline;
begin
  Result := Value * 2;
end;

var
  value: Integer;
begin
  value := Foo(10);
  ...
end;

このFoo関数がインライン展開されると、Fooの関数内にある処理がFoo(10)という呼び出しと直接置き換えられて、

  value := 10 * 2;

となります。さらに10も2も定数同士なので、定数畳み込みと呼ばれる最適化が施されて20となり、最終的には以下のように20を代入するコードが生成されます。

  value := 20;

これがインライン展開です。

インライン展開される条件が整っているにもかかわらずインライン展開が行われない場合というのは、つまりインライン展開される条件が整っているかどうか関係なく必ず関数呼び出しになる場合、と言い換えた方が分かりやすいかもしれません。そしてそのような場面はわりとあります。
「関数アドレスが必要になるので展開できない」のは分かりやすい例で、上述の中で言えばインターフェースから呼び出す場合と、コールバック関数として渡す場合が当てはまります。インターフェースの方は以前pikさんもハマってましたが、質問7にあるようにインターフェースというのは関数アドレステーブルなので、関数アドレスが絶対に必要になります。ループの条件式で展開されないのは最適化が難しいからでしょうか。
こういったインライン化の条件はヘルプにまとまっているので、そちらを読んでいただけると解説する手間が省けて助かります(結構あるので…)。

inline 指令の利用
http://docwiki.embarcadero.com/RADStudio/Berlin/ja/%E6%89%8B%E7%B6%9A%E3%81%8D%E3%81%A8%E9%96%A2%E6%95%B0%E3%81%AE%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97#inline_.E6.8C.87.E4.BB.A4.E3.81.AE.E5.88.A9.E7.94.A8

なお、ループの条件式ではなくループ内でも展開されないのではないかという回答がありましたが、これは展開されます。例えば、

function Foo: Integer; inline;
begin
  Result := 5;
end;

var
  i, value: Integer;
begin
  value := 0;
  for i := 1 to 100 do
    value := value + Foo;
end;

このような処理はFoo関数の呼び出しがインライン展開で定数(この場合は5)に置き換わります。

9.デフォルトのコンパイラ指令下において、RTTIでメソッド情報がほとんど取得できないのはどんな型か。またほとんどと書いたが、メソッドに関するどんな情報なら取得可能か。

インターフェース全般。デフォルト状態ではRTTIが生成されないので取得もできない。唯一メソッドの数だけが取得可能。

実行時型情報(RTTI)が生成されない型というだけであれば複数あるんですが、メソッドを持っていてかつRTTIが生成されない型というとインターフェースになります。元々はインターフェースだけでなくクラスやレコードもデフォルトではRTTIの生成対象外だったんですが、拡張RTTIが実装されてクラスとレコードはデフォルトでRTTIが生成されるようになりました。他にメソッドを持つ型というとクラスヘルパーとレコードヘルパーがありますが*2、これらもRTTIが生成されます(クラス扱い)。

メソッドの数は以下のようなコードで取得できます。

var
  p: PTypeInfo;
begin
  p := TypeInfo(IInterface);
  p^.TypeData^.IntfMethods.Count; // <= メソッドの数 
end;

ちなみにクラスの方はTPersistentを代表として$M+指令が指定されているとRTTIが生成されていましたが、インターフェースもこのコンパイラ指令があるとRTTIが生成されるようになります。紛らわしいんですが、$RTTI指令はクラスとレコードのみを制御するためにあるので、インターフェースでは依然として$M+が有効です。

RTTI 指令(Delphi
http://docwiki.embarcadero.com/RADStudio/Berlin/ja/RTTI_%E6%8C%87%E4%BB%A4%EF%BC%88Delphi%EF%BC%89

10.無名メソッド型の実態はInvokeメソッドを持つインターフェースであり、通常はAnonMethod()のようにそのまま実行できるが、Invokeメソッドを明示的に呼び出さないと実行できない場合がある。どんな時か。

ジェネリッククラスやメソッドで型パラメータに無名メソッド型を指定し、その型パラメータを型として指定された引数や変数などを実行しようとした場合。

具体的には以下のようなコードになります。

type
  TFoo = class
  public
    procedure Bar<T: TProc>(const Proc: T);
  end;

procedure TFoo.Bar<T>(const Proc: T);
begin
  // Proc(); // <= コンパイルエラー
  Proc.Invoke; // <= Invokeの明示的呼び出しが必要
end;

型制約に無名メソッド型を指定する意味はほぼ無いので、こんなコードはまず書くことはないんですが、万が一あった場合はコード補完でInvokeが出てくれないので、知ってないと詰みます。

おわりに

以上、解説でした。こちらの勘違いなどで実際とは挙動が異なることもあるかもしれませんが、そういう場合は是非ツッコミをお願いします。
それにしても、つ、つかれた…。書き上げるのに4時間かかった…。

*1:この辺りがvirtualはメモリを食いdynamicは遅いと言われる理由

*2:さらに言えばobject型もありますが扱いが酷く、拡張RTTI上ではなかったことにされてる感が…(エラー出たりとか)