2005-10-07
Cから継承した言語要素うち、列挙体は大きく仕様が変わったものの一つです。CとC++で共有するヘッダファイルで列挙体を使うと、思わぬ落とし穴にはまることがありますが、その辺を割り引いても、列挙体は非常に便利であり、C++では欠くことの機能になっています。
Cでは、列挙定数は必ずintでした
*1。しかし、C++では同じ列挙体に属している列挙定数(列挙子)がすべて収まるサイズになります
*2。そして、その型は、intやlongなどの汎用的な型ではなく、列挙体ごとに特別な型になります。これは、列挙体を用いることで、関数の多重定義ができることを意味しています。
列挙体を用いた多重定義は、タグ・ディスパッチと呼ばれる技法に使われることがあります。下の例を見てください。
#include <kernel.h>
enum forever_tag { forever = TMO_FEVR };
enum polling_tag { polling = TMO_POL };
inline ER wai_sem(ID semid, forever_tag)
{
return wai_sem(semid);
}
inline ER wai_sem(ID semid, polling_tag)
{
return pol_sem(semid);
}
inline ER wai_sem(ID semid, TMO tmout)
{
return twai_sem(semid, tmout);
}
上記はμITRONのセマフォの操作を多重定義したものです。
元のAPIでは、wai_semはセマフォが獲得できるまで永久に待ち続けるもの、pol_semは獲得できなければすぐエラーを返す(ポーリング)もの、twai_semはタイムアウト時間を指定できるものです。twai_semのタイムアウト時間としてTMO_FEVRやTMO_POLを渡せば、永久待ちやポーリングにも使えますが、それでは必要のないタイムアウト処理などもリンクされてしまうため、効率を重視して別のAPIが用意されています。
そこで、forever_tagおよびpolling_tagという列挙体を用意し、第二引数にそれらを受け取る関数を多重定義します。実際には、これらの引数は多重定義を解決するためだけに使用され、関数の中で使われることはありません。そして、wai_sem(semid, forever)とすれば(オリジナルの)wai_semが呼び出され、wai_sem(semid, polling)とすればpol_semが呼び出され、wai_sem(semid, 10)などと数値を渡せばtwai_semが呼び出されます。そして、単にwai_sem(semid)とすれば、オリジナルのwai_semが呼び出されます。
このようにすることで、待機方法をパラメータ化できる利便性を保ったまま、本当に必要な関数をコンパイル時に選択させることができるわけです。さすがに待機方法を一旦変数に格納してから渡す場合は、必ずtwai_semが呼ばれてしまいますが、変数を返す目的が、単に上位の関数から待機方法を指定したいためだけであれば、テンプレート引数を用いて、待機方法を渡すことも可能です。
template <typename Timout>
void foo(ID semid, Timout tmo)
{
ER ercd = wai_sem(semid, tmo);
... // 何らかの処理
sig_sem(semid);
}
列挙体のサイズが必ずしも一定ではなく、その列挙体に属している列挙定数がすべて収まるサイズになることは上でも書きましたが、列挙体を関数の引数や返却値型に使用した場合、その関数をCとの間のインタフェースにするのは危険です。なぜなら、CとC++で、その列挙体のサイズが異なるかもしれないからです。こうなると、深刻なバグに悩まされることになります。C結合(extern "C")にすることで、コンパイラが対応してくれてもよさそうなところですが、規格には何も書かれていないようです。
C++の列挙体は、静的な型チェックも強化されています。Cでは、列挙型の変数に普通の整数値を代入しても何の問題もありませんでしたが、C++では同じ型の値しか代入することができなくなっています
*3。また、列挙体に対しては増減演算子や複合代入演算子を使うこともできません。四則演算は普通にできますが、式の評価結果はもはや列挙体ではなく、普通の汎整数型になってしまいます。
また、クラス有効範囲で、そのクラス特有の定数値を定義するためにも列挙体は有効です。
class A
{
public:
enum { value = 123 };
};
多重定義のことも考えると、本来はstatic const int value = 123;のようにする方が望ましいのですが、やや古いコンパイラでは、const修飾された静的データメンバの初期化をクラス指定子の中で行うことができなかったという経緯もあり、代替手段として列挙体がよくつかわれます
*4。
*1 処理系の独自拡張で、longやlong long相当の値をとれる場合もあります。
*2 必ずしも最小のサイズになるとは限りません。
*3 無理やりキャストすればコンパイルできてしまいます。
*4 この技法はenumハックとして知られています。
Comment