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 も一瞬提案されましたが流されてしまいました。