適所で建設を

引数付きコンストラクタ

引数付きコンストラクタはどう書くんだい?

列挙型とバリアントレコードを組み合わせることになりますね。普通Adaの制約(constraint)は変更不可ですが、バリアントレコードに限ってタグにデフォルト値を与えておけば、Pascalみたいに後からタグを書き変えることもできるようになりますよ。

???……何を言ってるのかわからない。
僕が教えて欲しいのは引数付きコンストラクタの書き方なんだよ。
Ada.Finalization.Limited_Controlledを継承した型は、自動でInitializeが呼ばれるじゃない。でもこれだと引数を渡せないんだよね。

関数型言語かぶれの君のことですから、コンストラクタといえばバリアントのコンストラクタと思ったのですが。

何を言っているんだい、コンストラクタといえば、オブジェクトを初期化するアレの事に決まってるだろう。

ピュアな心を無くしたら終わりなのではなかったのですか……。

そんな古い話はどうでもいいよ。
どうやったらコンストラクタに引数を渡せるんだい。
それとも、まさか初期化ルーチンは普通のprocedureなのが普通なのかい。Text_IOやDirectoriesを見ても、CreateやStart_Searchを毎回呼ばせる形になってるから、少々不安なんだけど。
ちょっと調べたら制約のパラメータにする方法もあるみたいだけど、整数や列挙型しか使えないし……。アクセス型もいけるのかな、でも引数を別の構造体にパックしてポインタで渡すなんて、本当にそんなことをする必要あるのかい。

   type T (X : Integer) is -- 整数や列挙型、アクセス型しか使えないよ!
      new Ada.Finalization.Limited_Controlled with null record;
   Object : T (0);

確かにtaskなんかでは制約経由で引数を渡すしかありませんね……でも、普通の変数ならもっといい方法があります。

やっぱりね。それを教えてよ。

関数にしてオブジェクトを返すようにすればいいですよ。

limitedって言わなかったっけ?
代入は禁止したいんだ。

limitedでも、普通の関数でOKです。

そんな馬鹿な、なんのためのlimitedと思っているんだい。
それとも今日はネタ回答の日かい。困った奴だな。

いや、本当ですって……試してみなさいよ。

built-in-place

with Ada.Finalization;
procedure Test1 is
   type T is new Ada.Finalization.Limited_Controlled with record
      X : Integer;
   end record;
   function Ctor (X : Integer) return T is
      Result : T;
   begin
      Result.X := X;
      return Result;
   end Ctor;
   Object : T;
begin
   Object := Ctor (1);
end Test1;

test1.adb:10:14: (Ada 2005) cannot copy object of a limited type (RM-2005 6.5(5.5/2))
test1.adb:10:14: return by reference not permitted in Ada 2005
test1.adb:10:14: consider switching to return of access type
test1.adb:14:04: left hand of assignment must not be limited type

ほら見ろ、エラーになっただろう。
ええと……メッセージはaccess型にすることを考えろ、だって。
まさか関数の中でnewして返せなんてオチじゃないだろうね。明示的に解放しないといけなくなって、Limited_Controlledを継承してる意味が無くなるじゃないか。

Ada95まではlimited型を関数の返値にすると、C++で言う参照扱いでしたから、このエラーメッセージはその名残ですね。

参照があっても、ローカル変数の参照を返してもしょうがないし、意味ないなあ。

まあ、そうですね。
ちょっと貸してください……。(キーボードを奪う)

with Ada.Finalization;
procedure Test1 is
   type T is new Ada.Finalization.Limited_Controlled with record
      X : Integer;
   end record;
   function Ctor (X : Integer) return T is
   begin
      return (Ada.Finalization.Limited_Controlled with X => X);
   end Ctor;
   Object : T := Ctor (1);
begin
   null;
end Test1;

これでいいはずですよ。

さっきと同じに見えるなあ……。
ローカル変数をその場で展開して、後は代入を宣言と一緒にするように移動しただけじゃない。
こんなんで……あれ、コンパイルが通ったぞ?
どうなってるの?え?どういうこと?

Ada95にあった参照返しの機能が無くなって、Ada2005では、limited型を関数の返値にした場合の扱いはどうなったと思います?
すべての関数が、C++で言うコンストラクタ相当になりました。

えーと……どうなってるんだい、詳しく説明してくれ。

Result変数

まず、C++でコンストラクタが普通の関数と別になっているのはどうしてでしょうか。

そんなの決まってるじゃない。
普通の関数でオブジェクトを返したら、受け取る方でコピーが必要になるよ。

どうしてコピーが必要になるのですか?

関数の中で作ったローカルなオブジェクトと、受け取り側の変数は別だからさ。
ローカル変数でも、return文中で一時オブジェクトを作っても一緒だろう。
普通の関数が全く新しいオブジェクトを返すのに対して、コンストラクタは確保済みの領域の初期化を行うためのもの、と言えるね。
あれ、でもさっきは、ローカル変数だとエラーになって、一時オブジェクトなら通ったんだっけ?

ですね。
つまり、普通の関数で、呼び出し側が確保した領域にオブジェクトを作れるなら、普通の関数とコンストラクタの区別はいらなくなるというわけです。

え?そんなことしちゃっていいの?

いいも何も、言語を問わず大抵のコンパイラで、レジスタに収まりきらない値を返すときは呼び出し側の領域を隠しパラメータで受け取る形になってるはずですよ。
そうしないと、ややこしいスタックポインタの操作が必要になりますから。
Delphiという開発言語を知ってますか?

知ってるも何も、昔君にDelphiの美麗賛辞をさんざん聞かされて、もう耳タコなんだけどな……。

Delphiでは関数の返値がResult変数という形で陽に扱えるわけですが、レジスタに収まらない型の場合、このResult変数はそのまま呼び出し元の領域への参照でした。中を見てみたら代入先のはずの変数が見えたりしてね。それから……

(遮って)なるほど、Adaのreturn文中で作った一時オブジェクトは、呼び出し元の領域を使うわけだね。
受け取る方も、limited型でも初期化時の代入だけは特別に許可されている、と。
君がDelphiの話をすると終わらないからそれ以上の説明は要らないよ……。

そ、そうですか、話が早くて何よりです。
(昔書いた説明もありますので、そちらも参照ください)

そうそう、これ、当然newとも併用できるよね?

ええ。

   Ptr := new T'(Ctor (2));

拡張return文

この場合、Initializeはどうなるの?
一時オブジェクトが作られた後に呼ばれたりする?

aggregate式(括弧の中に要素を列挙して構造化型を作る式)の場合は、Initializeは呼ばれません。
あ、Finalizeのほうは変わらず呼ばれますから安心してください。

そうか、ひとつの式だけで初期化を済ませてしまわないといけないんだな。
ややこしいことをしようとすると難しいなあ……。
共通の初期化ルーチンを呼べたりすると便利なんだけど。別の関数を作って、引数で受け取ったオブジェクトの初期化ルーチンを呼んでから引数をそのまま返せば……あ、引数を返そうとする時点でコピーになるね……。

そんなあなたにResult変数をどうぞ。

Delphiの話はもういいってば。
Result変数方式が優れているのはもう何度も何度も何度も聞かされたけど、Adaはreturn文だし。

これもAda2005から、return文が拡張されて、return中に文が書けるようになりました。
勿論limited型でも使えます。

   return Result : T do
      ...
      Common_Initialize (Result);
      ...
   end return;

な、なんだってー。
「普通の関数」、万能過ぎる……。

空の制約

そうそう、引数付きコンストラクタが実現できるとわかった以上、デフォルトコンストラクタは禁止にしたいんだけど、どうすればいいかな。

privateな制約を付けたらどうですか。

こんな感じです。

private with Ada.Finalization;
package P is
   type T (<>) is limited private;
   function Ctor (X : Integer) return T;
private
   type Zero_Bit is range 0 .. 0; -- サイズ0の型
   for Zero_Bit'Size use 0;
   type T (Dummy : Zero_Bit) is new Ada.Finalization.Limited_Controlled with record
      X : Integer;
   end record;
end P;

外からは制約に具体的な値を渡せないので、Ctor経由で初期化するしかなくなる寸法です。

なるほどねえ、制約はこういう用途にも使えるのか。
サイズ0の型ね……確かCではだめだったような……。

Cで禁止されているのは、ポインタの値が後続オブジェクトと同じになってしまうからでしょうね。
Adaではaliasedを付けない限り、ポインタを取れない建前ですので、可能です。実際には色々抜け道がありますが……。

あ、でも待ってくれ。わざわざZero_Bitを作らなくても、こうでいいんじゃないか?

private with Ada.Finalization;
package P is
   type T (<>) is limited private;
   function Ctor (X : Integer) return T;
private
   -- これでもコンパイル通るよね
   type T is new Ada.Finalization.Limited_Controlled with record
      X : Integer;
   end record;
end P;

ええ、それでいいはずなんですが……。
表向き制約を持っている、と宣言されている型が実際に制約を持ってないと、GNATのバグを踏んだ記憶が……いえ、もう直ってるかもしれません……。

……。

セカンダリスタック

ん?
制約付きの型でもそのbuilt-in-placeは使えるんだよね。
制約によって、サイズが変わるような型ならどうなるんだい。

private with Ada.Finalization;
package P is
   type T (<>) is limited private;
   function Ctor (X : Integer) return T;
private
   type T (X : Integer) is new Ada.Finalization.Limited_Controlled with record
      Data : String (1 .. X); --  Xが決まるとサイズも決まる
   end record;
end P;

この型に必要なサイズは、Ctor中のreturn文まで行かないと決まらないはずだよ。
大きめの領域を確保するにしたって、Xが1や2の時もあれば何十万なんて時もあるだろうし。
あらかじめ呼び出し元が領域を確保しておくのは無理がないか。
……でもコンパイル通るね……。

通りますね。

どうやってるの、これ?
「ややこしいスタックポインタの操作」をしてるのかなあ。

allocaのような動作をしてくれれば効率良くて嬉しいのですが、残念ながら、gccのバックエンドは可変長返値をサポートしてないですね……。

-Sで見てみるか。
やや、_system__secondary_stack__ss_allocateってのを呼んで領域を確保しているみたいだ。
何か特殊なことをする関数かな。

セカンダリスタックですね。
GNATは、普通のスタックの他にもうひとつスタックを用意していて、可変長の値を返す場合はそっちを使うようになってます。
FORTHのデータスタックとコールスタックが別になっているみたいなものでしょうか。

GNATも凄いことをやってるんだなあ……。
正直バグが多いので馬鹿にしかけてたよ。

苦肉の策っぽいといいますか、ソースを見るとセカンダリスタックもmallocで取ってたりして適当感が。

君はいつも一言多いんだよ、折角褒めてるのに。