8.1 enum

enumは列挙型とも言われます。enumを用いることである一定の値しかとることのできない型を作る事ができるのです。文法を以下に示します。

enum 識別子名 スコープ{
    enumerator-list
}オブジェクト識別子;

このように記述する事で、その識別子名のenum型を作る事ができます。

8.1.1 enumの基本概念

まあまあ取り敢えず、以下のコードを見てみましょう。

enum Color{
    Red,
    Green,
    Blue
};

void print(Color cl)
{
    std::cout<<cl<<std::endl;
}

int main()
{
    print(Red);
    print(Green);
    print(Blue);
}

実行結果は以下となります。

0
1
2

Colorという名前のenumを定義し、そのenumerator-listにRedGreenBlueと定義しています。この時、enumerator-listに定義した順に、0 1 2 3 4...と各enumeratorに定義されます。よって、Red0Green1Blue2となります。そして関数printの引数型には、Colorというenum型を指定しています。よってこの関数に渡せるデータは、Color型のオブジェクトのみとなります。 以下のように、Color型以外の値は、渡す事ができません。

enum Color{
    Red,
    Green,
    Blue
};

void print(Color cl)
{
    std::cout<<cl<<std::endl;
}

int main()
{
    print(42); // 42はColor型の値ではないため呼び出せない。エラー
}

さて、上記のコードはenumの機能をまだ使い切っていません。次のサンプルコードを見てみましょう。

#include<iostream>
enum Param{
    A=1,B,C,D=2,E,F
};

void print(Param cl)
{
    std::cout<<cl<<std::endl;
}

int main()
{
    print(A);
    print(B);
    print(C);
    print(D);
    print(E);
    print(F);
}

実行結果は以下となります。

1
2
3
2
3
4

enumerator-listの中で=1だとか=2だとか指定されています。実行結果と照らし合わしてみると挙動が分かるかもしれません。Aには1を指定しています。よってB2C3です。その後なにも指定されなければ、D4になりますが、Dには2と指定されています。よって、D2となり、EDからの列挙値として3F4となるのです。

さてさてどんどん行きましょう。次のサンプルコードを見てください。

#include<iostream>
enum Param{
    A=1,B,C
}par; // #1

void print(const Param& cl) // 勿論const参照にする事もできる。
{
    std::cout<<cl<<std::endl;
}

int main()
{
    par=A; print(par);
    par=B; print(par);
    par=C; print(par);
    //par=42; は42がParam型でないため代入できない。エラーとなる。

    Param par1=A; print(par1); // #2
    // par1=B; print(par1); ... 略

    enum Param1{ // #3
        D,E,F
    };
}

実行結果は以下となります。

1
2
3
1

注視してほしいところに、それぞれマークをつけました。まず #1についてですが、enum Paramと定義した;の前に、parという記述がありますね。ここに識別子を記述すると、そのenumのオブジェクトをその場で定義する事ができます。この時parは、グローバルスコープに定義されます。そのparに対してmain関数でParamのenumerator-listを順次代入して出力させています。

次に#2ですが、このように型 識別子;というような今までの書き慣れた方式で勿論enumのオブジェクトを定義する事が可能です。それをenumerator-listにあるAで初期化し出力させています。

最後に#3です。なんと、関数内でenumを定義しています。これも、正しいコードです。このように関数内でenumを定義した場合、そのenumはその関数内でしか使う事ができません。「第5章 スコープと制御文」でも述べた、スコープの概念を基に考えると、自然に思える挙動です。

さて、次がこの項の最後のサンプルコードです。見て行きましょう。

#include<iostream>
enum Param{
    A,B,C
};

void print(int p)
{
    std::cout<<p<<std::endl;
}

int main()
{
    print(A);
    print(B);
    print(C);
}

実行結果は以下となります。

0
1
2

Paramというenumを定義しています。enum内に定義されたenumerator-listは、そのenumでしか受け取れないはず...ですが、なんと関数printintを受け取る関数であるにも関わらず呼び出せています。何故でしょうか?

この理由は単純で、スコープなしのenumは整数型(signedunsigned関わらず)に暗黙的に変換する事が可能だからです。暗黙的に変換が可能な特徴は、よく覚えておきましょう。enumを使った時に、思いもよらぬ関数がオーバーロード解決されたら、もしかするとこのような暗黙変換が働いている可能性があるからです。ところで、スコープなしのとはなんなのでしょうか。enumにスコープ?そんなものがあるのでしょうか。

実は、スコープを付与する事もできるのです。

8.1.2 スコープ付きenum

enumは以下のように使えると述べました。

#include<iostream>
enum Param{
    A,B,C
};
int main()
{
    std::cout<< A <<" "<< B <<" "<< C <<std::endl; // 0 1 2
}

ここまでで、既にお気づきになったかもしれませんが、グローバル領域に定義されたenumはそのenumerator-listもグローバル領域となります。よって、enum自体が異なる識別子だとしてもenumerator-listが同名だった場合、ODRに違反してしまうのです。

enum Param{
    A,B,C
};
enum Param1{ // Param1というようにenumの識別子は異なるが...
    A,B,C // enumerator-listは同名なため判別出来ずエラー
};

これを回避するためには、まず一つ目の策としてnamespaceを使うという手があります。

namespace nm{
    enum Param{
        A,B,C
    };
}

namespace nm1{
    enum Param1{
        A,B,C
    };
}

int main()
{
    nm::Param pm=nm::A; // 名前空間で完全修飾
    nm1::Param1 pm1=nm1::A; // 名前空間で完全修飾
}

しかし、この方法は代替的な案であり、根本的な解決とはなっていません。私たちが示したいのはどの名前空間のenumerator-listかではなく、どのenumのenumerator-listかなのです。つまり、以下のように記述したいわけです。

enum Param{
    A,B,C
};
enum Param1{
    A,B,C
};
int main()
{
    Param::A; // ParamのA
    Param1::A; // Param1のA
}

これを実現させるのはとても簡単です。以下のように記述します。

enum class Param{
    A,B,C
};
enum struct Param1{
    A,B,C
};
int main()
{
    Param::A;
    Param1::A;
}

このように、enumの後にclass、もしくはstructキーワードを付与する事で、そのenumがスコープを持つenumとして定義されます。classstruct、どちらのキーワードを使っても差異はありません。 このようにスコープを持つenumのことをscoped enumeration type、スコープを持たないenumのことをunscoped enumeration typeと言います。そのままですね。

スコープを持つenum、scoped enumeration typeにはいくつか特徴があります。例えば、先ほどまで述べていたunscoped enumeration typeでは整数型への暗黙の変換が許されていました。

enum Param{
    A
};
int main()
{
    int a=A; // OK
}

しかし、scoped enumeration typeでは、このような暗黙の変換は許されません。

enum class Param{
    A
};
int main()
{
    int a=Param::A; // Bad. エラー
}

また、unscoped enumeration typeでは、enumerator-listに明示された識別子に対して定義される値の型を明示する事はできませんでした。scoped enumeration typeではその基底型を指定する事ができます。

#include<iostream>

enum class Param:unsigned int{ // 基底型をunsigned intにする
    A=-1,B,C // エラー!Aがunsignedではない。
};

void print(Param p)
{
    std::cout<<static_cast<unsigned int>(p)<<std::endl;
}

int main()
{
    print(Param::A);
    print(Param::B);
    print(Param::C);
}

このように基底型を指定できる事で、誤った値を設定してしまう事を防ぐ事ができる他に、どのような値を扱うのか型を明記する事でその明示的にする事ができるのです。

scoped enumeration typeの最後の特徴としては、前方宣言ができる事です。unscoped enumeration typeでは、前方宣言をする事ができません。

#include<iostream>
enum class Param:unsigned int; // 前方宣言

void print(Param p)
{
    std::cout<<static_cast<unsigned int>(p)<<std::endl; // 整数型への暗黙変換は効かないため明示的にキャストする
}

enum class Param:unsigned int{
    A,B,C
};

int main()
{
    print(Param::A);
    print(Param::B);
    print(Param::C);
}

実行結果は以下となります。

0
1
2

このように、scoped enumeration typeはunscoped enumeration typeに比べて出来ることが多いですし、型についての厳格性や意味論を深めるだけでなく、名前衝突が回避できたりと、とても優れた面が多いです。可能な限り、scopedなenumerationを用いることをお勧めします。

8.1.3 無名enum

無名なenumを定義する事も勿論可能です。

enum{value}; // 無名enum

int main()
{
    value; // 0
}

無名enumはunscoped enumeration typeの特性に加えて型情報も一切含まない形態です。一般的に無名enumは、値が定数である事を強調したい時に用いられるシーンが多いです。例えば、以下のように。

int main()
{
    enum{size=42;}
    int ar[size];
}

配列の定義時、指定する要素数は定数でなければなりませんが、enumはまさに定数ですので、この要件を満たしているのです。

8.1.4 enumの文法についてもう一言

オブジェクト識別子とそのスコープ

ここで、今一度enumの文法を見直してみましょう。

enum 識別子名 スコープ{
enumerator-list
}オブジェクト識別子;

最後のオブジェクト識別子とはどのように使うのでしょうか。

enum X{
    A,B,C
}data; // here

int main()
{
    data=C;
}

このようにした時、X型のdataというオブジェクトがそのenumのあるスコープ内で生成されます。上記の場合、dataはどのスコープにも所属していないため、グローバル変数として定義される事となります。つまり、以下の記述と動作は同じになります。

enum X{
    A,B,C
};

X data;

int main()
{
    data=C;
}

これは、スコープ付きenumでも同様です。

enum struct X{
    A,B,C
}data;

int main()
{
    data=X::C;
}

オブジェクト識別子を定義するとともに、初期化を行うこともできます。

enum X{
    A,B,C
}data=C; // dataをCで初期化

スコープ付きenumも同様です。

enumを関数内に定義する

enumを関数内に定義することもできます。

void f()
{
    enum X{A,B,C};
    enum Y{D,E,F}y=F;
    X x=C;
}

int main()
{
    f();
}

この場合、定義自体もスコープの概念をもつため、関数fの処理後にX型とY型は破棄されます。スコープ付きenumも同様です。

オブジェクト生成の際のenumキーワード

enumで定義した型は「2.1.6 データ型の一覧」に記載されているような型のようにオブジェクトを生成することができました。

enum struct X{A};

int main()
{
    X x; // enum X型のオブジェクトxを生成
}

これは、以下のように記述することもできます。

enum struct X{A};

int main()
{
    enum X x; // enum X型のオブジェクトxを生成
}

enumの型名であるXの前にenumキーワードが付与されています。これは、型Xenumであることを明示するための記法で、C言語(厳密にはC89まで)の仕様から受け継がれた記法です。

しかしC++では、多くの場合enum X x;というようにキーワードを付与することはあまりありません。