前置き
プログラミング言語 C における列挙型の扱いについての疑問を読みました。
》 [WIP] 組込みC > enumと#defineの違い
元記事のコメントに書いた通り C では enum
で定義した列挙定数は int
型です。 列挙型はそれぞれ独自の型で、その大きさは同時に定義した列挙定数全てを格納可能な大きさです。
しかし、 C++ では解釈が少し異なります。 この記事では C と C++ の間で列挙定数がどのように違うのかということと、 C++ での列挙定数の活用事例を紹介するものです。
C++ での解釈
C++ では列挙定数はその列挙定数を定義したときの列挙型です。
たとえば
enum foo {
bar,
baz
};
void qux(void) {
int foobar=bar;
}
という定義があったとき、 C における bar
は int
型で C++ における bar
は enum foo
型となります。
clang の構文解析結果を眺めてみよう
clang は構文解析段階での結果を可視化する機能があります。 上述の例がどう解釈されているか観察するには以下のようなコマンドを入力してください。
clang -cc1 -fsyntax-only -ast-dump sample.c
このような結果を得られます。
TranslationUnitDecl 0x45cb110 <<invalid sloc>> <invalid sloc>
|-TypedefDecl 0x45cb3e8 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'char *'
| `-PointerType 0x45cb3c0 'char *'
| `-BuiltinType 0x45cb170 'char'
|-EnumDecl 0x45cb418 <test.c:2:1, line:5:1> line:2:6 foo
| |-EnumConstantDecl 0x45cb490 <line:3:3> col:3 referenced bar 'int'
| `-EnumConstantDecl 0x45cb4d0 <line:4:3> col:3 baz 'int'
`-FunctionDecl 0x45cb580 <line:7:1, line:9:1> line:7:6 qux 'void (void)'
`-CompoundStmt 0x45cb690 <col:16, line:9:1>
`-DeclStmt 0x45cb680 <line:8:3, col:17>
`-VarDecl 0x45cb630 <col:3, col:14> col:7 foobar 'int' cinit
`-DeclRefExpr 0x45cb668 <col:14> 'int' EnumConstant 0x45cb490 'bar' 'int'
最後の行を見ると bar
が int
として解釈されているということがわかると思います。
では、 C++ として解釈させた場合はどうでしょうか。 -x
オプションで言語を強制的に指定します。
clang -cc1 -fsyntax-only -ast-dump -x c++ sample.c
それで得られる結果は以下です。
TranslationUnitDecl 0x459c138 <<invalid sloc>> <invalid sloc>
|-TypedefDecl 0x459c428 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'char *'
| `-PointerType 0x459c400 'char *'
| `-BuiltinType 0x459c190 'char'
|-EnumDecl 0x459c458 <test.c:2:1, line:5:1> line:2:6 foo
| |-EnumConstantDecl 0x459c4d0 <line:3:3> col:3 referenced bar 'enum foo'
| `-EnumConstantDecl 0x459c510 <line:4:3> col:3 baz 'enum foo'
`-FunctionDecl 0x459c5c0 <line:7:1, line:9:1> line:7:6 qux 'void (void)'
`-CompoundStmt 0x459c6e0 <col:16, line:9:1>
`-DeclStmt 0x459c6d0 <line:8:3, col:17>
`-VarDecl 0x459c670 <col:3, col:14> col:7 foobar 'int' cinit
`-ImplicitCastExpr 0x459c6c0 <col:14> 'int' <IntegralCast>
`-DeclRefExpr 0x459c6a8 <col:14> 'enum foo' EnumConstant 0x459c4d0 'bar' 'enum foo'
bar
は enum foo
型になっていますね。 さらに int
型の変数 foobar
を初期化するにあたって暗黙の型変換で int
に変換されています。 C とは解釈が違うことがよくわかります。
C/C++ には未定義や処理系定義の箇所、つまり処理系に裁量がゆだねられている項目もあるので clang での解釈が C/C++ の規格であるというわけではありませんが、処理系がどのように解釈しているのか手っ取り早く確かめるには clang の -ast-dump
オプションはとても便利です。
型の違いを利用する
C++ での列挙定数は列挙型ごとに独立した型であるとはいえ、暗黙の型変換で滑らかに整数型になってしまうので C での解釈との違いを意識することはほとんどないでしょう。 しかし、型が違うことを利用して変則的なタグディスパッチに活用できます。
例としてフラグで挙動を変更するような関数を考えてみます。
#include <iostream>
enum flag {
one,
two,
three
};
void print(enum flag flag) {
switch(flag) {
case one: std::cout << "one" << std::endl; break;
case two: std::cout << "two" << std::endl; break;
case three: std::cout << "three" << std::endl; break;
}
}
int main(void) {
print(one);
print(two);
print(three);
return 0;
}
しかし、フラグの類は決め打ちで書くことも多く、コンパイル時に確定してしまうにもかかわらずスイッチで分岐するのは余計な処理です。 fopen
などを考えてみれば、ファイル名は実行時まで決まらないということはよくあっても、読出しモードと書込みモードを実行時に切り替えるということはまずないでしょう。 しかし、確実にないと言い切れるほどでもありません。 コンパイル時に確定してしまうフラグと実行時までわからないフラグを両立するのに列挙型を使うとこう書けます。
#include <iostream>
enum one {one=1};
enum two {two=2};
enum three {three=3};
void print(enum one) {
std::cout << "optimized one" << std::endl;
}
void print(enum two) {
std::cout << "optimized two" << std::endl;
}
void print(enum three) {
std::cout << "optimized three" << std::endl;
}
void print(int flag) {
switch(flag) {
case one: std::cout << "one" << std::endl; break;
case two: std::cout << "two" << std::endl; break;
case three: std::cout << "three" << std::endl; break;
}
}
int main(void) {
print(one);
print(two);
print(three);
int flag=three;
print(flag);
return 0;
}
整数値のフラグで処理を分けているかのようにふるまいながらも、決め打ちで書いた場合には最適化された処理を実行します。
列挙型は個別の型を成立させると同時に整数でもあるという中途半端さがありますが、その中途半端さにも活用できる価値があるというわけです。