Ada 202x (13日目) - 'Enum_Rep/'Enum_Val ======================================= .. post:: Dec 13, 2019 :tags: ada, ada_2022 今回こそは簡単なやつ。 経緯 ---- 列挙型には内部表現を指定することができます。 .. code-block:: ada 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`` でキャストする例。 .. code-block:: ada 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`` 属性は動的な値扱いになってしまいますから上の例のようにそのまま使うことができません。 .. code-block:: ada 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はそのようです `_\ 。 今までどうしていたか -------------------- 回避策としてはまあこんな感じでして……。 .. code-block:: 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`` 属性が追認されています。 それぞれ列挙型から内部表現へ、内部表現から列挙型への変換です。 .. code-block:: ada 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: ビット集合 ++++++++++++++ .. code-block:: ada -- 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言語の仕様では代入で入れる分にはセーフです。念の為。) | https://gcc.gnu.org/bugzilla/show_bug.cgi?id=67433 NG: 後で増えそうなものとFFI +++++++++++++++++++++++++++ .. code-block:: c typedef enum { NO_ERROR = 0, ERROR_A, ERROR_B, ERROR_C } error_code; error_code get_error(void); .. code-block:: ada -- 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言語のライブラリなんてエラーコードを返すはずの関数がマイナス値に特殊な意味を持たせてきたり等も平気でしてきますので、素直に列挙型を対応させますと碌な事になりません。 .. code-block:: ada 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: 間の値を使う ++++++++++++++++ .. code-block:: ada -- 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`` も一瞬提案されましたが流されてしまいました。 .. _`AI12-0237-1`: http://www.ada-auth.org/cgi-bin/cvsweb.cgi/ai12s/ai12-0237-1.txt