Ada 202x (13日目) - 'Enum_Rep/'Enum_Val¶
今回こそは簡単なやつ。
経緯¶
列挙型には内部表現を指定することができます。
type Enum is (A, B, C);
for Enum use (A => 11, B = 123, C => 4000);
X : Enum := A;
メモリ上ではこの内部表現で格納されます。
他言語やメモリマップドI/Oに渡すぶんにはそれでいいのですが、Ada内で内部表現の値を取り出すには Ada.Unchecked_Conversion
で値キャストするか、メモリ操作でアクセスするかしかありませんでした。
('Pos
属性で取り出せる値は内部表現に影響されず常に連番です。)
Ada.Unchecked_Conversion
でキャストする例。
type Repr_Of_Enum is mod 2 ** Enum'Size;
for Repr_Of_Enum'Size use Enum'Size;
function Cast is new Ada.Unchecked_Conversion (Enum, Repr_Of_Enum);
Repr_Of_X : Repr_Of_Enum := Cast (X);
メモリ操作でアクセスする方法は多岐に渡りますので省略。 効率はキャストの方が良いはずです。
それで、キャストするにしろメモリアクセスするにしろ、変数のサイズを一致させないといけないわけです。
特に列挙型が generic
の引数だったりしますと型引数の 'Size
属性は動的な値扱いになってしまいますから上の例のようにそのまま使うことができません。
generic
with T is (<>);
function Generic_Repr (X : T) return Integer;
function Generic_Repr (X : T) return Integer is
type Repr_Of_T is mod 2 ** T'Size;
for Repr_Of_T'Size use T'Size; -- error
function Cast is new Ada.Unchecked_Conversion (T, Repr_Of_T);
begin
return Integer (Cast (X));
end Generic_Repr;
規格では generic
の実装モデルとしてtemplateのようにコードを展開するやり方だけではなく、MLのように異なる型のインスタンスがコードを共有するやり方も想定されていて、Randy先生のJanus/Adaはそのようです。
今までどうしていたか¶
回避策としてはまあこんな感じでして……。
function Generic_Repr (X : T) return Integer is
begin
if T'Size <= 8 then
declare
type Repr_Of_T is mod 2 ** 8;
for Repr_Of_T'Size use 8;
function Cast is new Ada.Unchecked_Conversion (T, Repr_Of_T);
begin
return Integer (Cast (X));
end;
elsif T'Size <= 16 then
declare
type Repr_Of_T is mod 2 ** 16;
for Repr_Of_T'Size use 16;
function Cast is new Ada.Unchecked_Conversion (T, Repr_Of_T);
begin
return Integer (Cast (X));
end;
else
-- 以下略
end if;
end Generic_Repr;
この泥臭い方法も T'Size
が8,16,...ビットであること前提で、9ビットとか寄越されるとどうしようもありませんでした。
1から64まで全パターン書けばいいって?
ハハッ。
一応擁護しておきますとtemplate式のコンパイラでは generic
内であっても T'Size
は規格上の扱いは動的でも実際には静的に決まりますので最適化されるはずです。
Ada 202xでの改善¶
'Enum_Rep/'Enum_Val¶
GNATの拡張だった 'Enum_Rep
属性と 'Enum_Val
属性が追認されています。
それぞれ列挙型から内部表現へ、内部表現から列挙型への変換です。
function Generic_Repr (X : T) return Integer is
begin
return T'Enum_Rep (X);
end Generic_Repr;
簡単ですね! 今日はおしまい!
関連AI¶
AI12-0237-1 Getting the representation of an enumeration value
使ってはいけない列挙型の使い方¶
これで済ませたかったのですけれども、ちょっと余計なことを書かせてください。 巷のコードを漁っていますと、どうにも良くない列挙型の使い方が散見されます。 この拡張によって良くない使い方に拍車がかかるといけませんので、NG集を書かせてください。
NG: ビット集合¶
-- This is BAD example!
type Flags is (A, B, C);
for Flags use (A => 1, B => 2, C => 4);
X : Flags :=
Flags'Enum_Val
(Flags'Enum_Rep (A) + Flags'Enum_Rep (B) + Flags'Enum_Rep (C));
なまじC#で [Flags]
としてサポートされている使い方なので性質が悪いです。
AdaではNGです。
といいますのは、列挙型に範囲外の値が入ることは想定されていないからです。
コンパイラは列挙型の値が列挙された値の何れかであることを利用した最適化を行いますので大変危険です。
GNATでは実行時にチェックを行うこともできます。
デフォルトでOFFになっていますので -gnatVa
オプションを付けてお試しください。
似たようなバグでC言語のbool型に0,1以外をmemset等で直接入れたらundefined behavior、という実例があります。 (C言語の仕様では代入で入れる分にはセーフです。念の為。)
NG: 後で増えそうなものとFFI¶
typedef enum { NO_ERROR = 0, ERROR_A, ERROR_B, ERROR_C } error_code;
error_code get_error(void);
-- This is BAD example!
type error_code is (NO_ERROR, ERROR_A, ERROR_B, ERROR_C);
function get_error return error_code
with Import, Convention => C;
C言語のライブラリをインポートしているとありがちです。
ERROR_D
が追加されたときにAda側を追従、再コンパイルできなければ上と同じ状況になります。
そうでなくてもC言語のライブラリなんてエラーコードを返すはずの関数がマイナス値に特殊な意味を持たせてきたり等も平気でしてきますので、素直に列挙型を対応させますと碌な事になりません。
NO_ERROR : constant := 0;
ERROR_A : constant := 1;
ERROR_B : constant := 2;
ERROR_C : constant := 3;
function get_error return Interfaces.C.int
with Import, Convention => C;
こうした方が無難です。
NG: 間の値を使う¶
-- This is BAD example!
type Enum is (A_Base, B_Base, Last);
for Enum use (A_Base => 0, B_Base => 100, Last => 200);
X : Enum := Flags'Enum_Val (Flags'Enum_Rep (B_Base) + 30));
有効な値の間であっても範囲外です。 領域を予約したことにはなりません。 これもC言語や、同じPascal系でもDelphiでは有効な使い方なので性質が悪いです。
所感¶
独自実装の追認とはいえ 'Pos
と 'Val
と対にするため 'Enum_Rep
は他の名前の方が良かったのでは、と思わなくもないです。
実際に 'Enum_Rep_Pos
や 'From_Enum_Rep
も一瞬提案されましたが流されてしまいました。