C++ enum class【C++で導入された新しい列挙型の使い方】

C++
この記事は約14分で読めます。

こんにちは、ナナです。

「enum」とは、列挙型という連番を生成するための機能であり、C言語でもC++でも利用することが可能です。
>>『C言語 enum 列挙型【連番の作り方と使いどころを教えます】

しかし、「enum」には様々な課題もあり改善の余地がありました。

C言語における「enum」の課題
  • 列挙子が名前衝突を起こしやすい
  • 列挙子で定義した以外の値を列挙型変数に格納することができる
  • 列挙子の代わりに整数値を利用できる

C++ではこれらの課題を解決するため、既存の「enum」を残しつつ、新たに「enum class」という機能を新設しました。
※ 「enum class」はC++11以降に対応したコンパイラで利用可能

これにより、C++では次の2つの列挙型を利用できることになります。

種類スコープ有無備考
enumスコープなし列挙型C言語で利用できるenumとほぼ同じ列挙型
enum classスコープ付き列挙型C++で新規追加された参照範囲の指定が必要な列挙型

「enum」と「enum class」との違いはどのようなものなのかを比較しながら、この新機能を学んでいきましょう。

本記事で学習できること
  • 「enum class」の定義方法とは?
  • 「enum class」の特徴と「enum」との違いとは?
  • 「enum class」を使う時の注意点とは?
  • 「enum class」を整数型として扱う方法とは?
  • C++の「enum」とC言語の「enum」の違いとは?
スポンサー

「enum class」の定義方法と「enum」との使い方の違い

それでは、C言語による「enum」と、C++の「enum class」の定義方法と使い方を比べてみましょう。

typedef enum 
{
	Poodle,
	Shiba,
	Chihuahua,
	Bulldog,
} E_Dog;

int main(void)
{
	E_Dog dog = Poodle;

	return 0;
}
enum class E_Dog
{
    Poodle,
    Shiba,
    Chihuahua,
    Bulldog,
};

int main()
{
    E_Dog dog = E_Dog::Poodle;

    return 0;
}

C++では「enum class」型を定義するのに「typedef」は不要です。定義に関してはさほど違いはありませんね。

そして定義を使う側では、「enum class」の場合はスコープ解決演算子を使って「型名::列挙子」として指定する必要があります。

ナナ
ナナ

「enum class」がスコープ付き列挙型と呼ばれるのは、このようにスコープ解決演算子を使ってスコープ指定を行う必要があるためです。

「enum class」は「enum struct」とも書ける

「enum class」の定義は次のように「enum struct」と書くこともできます。

enum struct E_Dog
{
    Poodle,
    Shiba,
    Chihuahua,
    Bulldog,
};

int main()
{
    E_Dog dog = E_Dog::Poodle;

    return 0;
}

機能的には同じですので、どちらを使っても一緒です。

列挙子にスコープ指定が必要なのは面倒?

「enum」での列挙子はそのまま利用できましたが、「enum class」ではスコープ解決演算子を使った型名を指定する必要があります。

E_Dog dog = Poodle;
E_Dog dog = E_Dog::Poodle;

「enum class」の書き方に対し、「わざわざ型名を指定するって面倒!」とわずらわしさを感じる方もいるかもしれません。

しかし、名前衝突の観点からもこれは良き改定と考えてください。

C言語における「enum」の列挙子はグローバルなスコープを持つため、他の定義との名前衝突の危険性が高いのです。

実践的には、次のように名前衝突を避けるため、列挙子名の先頭に型名といったプレフィックス(接頭語)を付与して回避することになります。

typedef enum
{
	E_Dog_Poodle,   // 先頭にプレフィックスを入れて名前衝突の確率を軽減する
	E_Dog_Shiba,
	E_Dog_Chihuahua,
	E_Dog_Bulldog,
} E_Dog;

「enum class」においては、「列挙子」が型名の中に階層的に所属するためプレフィックスを付与する必要がなくなりました。

列挙子の所属関係

つまり、「型名」の名前衝突を防止すれば、「列挙子」は名前衝突することがなくなったわけです。

ナナ
ナナ

大元の「型名」側の名前衝突を防止すれば、所属する列挙子の名前衝突を考える必要がなくなるわけです。

数の多い「列挙子」に対して、数が少ない「型名」の方が名前衝突を回避しやすいのです。

スポンサー

「enum class」の特徴と「enum」との違い

「enum class」が「enum」からどのように進化したのかを解説しましょう。

「enum class」では管理するためのデータ型を指定できる

「enum」はint型をベースにした型となっていますが、「enum class」では皆さんが管理するためのデータ型を指定することが可能です。

enum class E_Dog : short
{
    Poodle,
    Shiba,
    Chihuahua,
    Bulldog,
};

このように、型名の後ろに「:(整数型)」を指定することで、管理するデータ型を選ぶことができます。

データ型の指定は次のルールがあることに注意しましょう。

enum class のデータ型指定時のルール
  • 指定できる型は「整数型」のみ(double型といった浮動小数点型は指定できない)
  • 型指定を省略したときは「int型」となる
  • 「unsigned」系の型を指定した場合は、負値の値を設定できない

「enum class」では列挙子以外の値を設定できない

次のように「enum」では整数値を直接代入したり、列挙子範囲外の整数値も入れることが可能でした。

typedef enum
{
    E_Dog_Poodle,       // 0
    E_Dog_Shiba,        // 1
    E_Dog_Chihuahua,    // 2
    E_Dog_Bulldog,      // 3
} E_Dog;

int main(void)
{
    E_Dog dog = E_Dog_Poodle;

    dog = 1;    //  E_Dog_Shiba の代わりに「1」の代入もOK
    dog = 10;   //  範囲外の値の代入もOK

    return 0;
}

このように「enum」ではデータ型というものint型相当の整数型として管理しているため、利用する側次第で意図しないデータを設定できる問題がありました。

この問題に対して、「enum class」では厳しくデータ型をチェックするようになりました。

enum class E_Dog
{
    Poodle,
    Shiba,
    Chihuahua,
    Bulldog,
};

int main()
{
    E_Dog dog = E_Dog::Poodle;

    dog = 1;        //  ビルドエラー:Shibaの代わりに代入できない!
    dog = 10;       //  ビルドエラー:範囲外の整数値も代入できない!

    return 0;
}

このように列挙子以外の整数値を代入したり、判定したりすることはできなくなりました。

この改定は、型の正当性という観点から管理が安全になったと言えるでしょう。

ナナ
ナナ

「enum」では型を定義しているのに、もしも定義外の値を設定されたらという可能性をケアする必要がありましたが、「enum class」ではコンパイラが不正をチェックしてくれます。

スポンサー

「enum class」を使う時の注意点

「enum class」は厳密な型を管理する列挙型となりましたが、それゆえに「enum」でできていたことができなくなる部分も出てきます。

配列インデックスに利用できない

列挙型はデフォルトでは、0から始まる連番で値が定義されます。

この性質を利用して、配列インデックスに列挙子を利用する方法がありますが、「enum class」では指定できません。

enum class E_Dog
{
    Poodle,     //  0
    Shiba,      //  1
    Chihuahua,  //  2
    Bulldog,    //  3
};

int main()
{
    int num[4] = { 0 };

    num[E_Dog::Poodle] = 100;   //  ビルドエラー

    return 0;
}

加減算やインクリメント・デクリメントができない

「enum」では加減算といった四則演算が可能でしたが、「enum class」ではできません。

enum class E_Dog
{
    Poodle,     //  0
    Shiba,      //  1
    Chihuahua,  //  2
    Bulldog,    //  3
};

int main()
{
    E_Dog dog = E_Dog::Poodle;

    dog = dog + 2;  //  ビルドエラー:加減算ができない
    dog++;          //  ビルドエラー:インクリメント・デクリメントはできない

    return 0;
}

このように「enum class」は整数型ではあるものの、プリミティブな整数型とは異なる型として認識されており、組み合わせて算術することができなくなっています。

列挙型を利用したテーブル管理ができない

「enum class」が算術や配列インデックスに利用できない場合、次のような列挙子を配列インデックスとして管理するテーブル構造のデータが利用できなくなります。

#include <iostream>

enum class E_Dog
{
    Poodle,     //  0
    Shiba,      //  1
    Chihuahua,  //  2
    Bulldog,    //  3
};

typedef struct
{
    char jpn[32];  // 日本語名
    char eng[32];  // 英語名
} S_DOG;

// 日本語・英語名の文字列テーブル
const S_DOG dogTable[] =
{
    {"プードル",   "Poodle" },
    {"柴犬",       "Shiba" },
    {"チワワ",     "Chihuahua" },
    {"ブルドッグ", "Bulldog" },
};

int main()
{
    // 配列インデックスに列挙子を使って文字を表示
    for (E_Dog dog = E_Dog::Poodle; dog <= E_Dog::Bulldog; dog++)    //  ++は使用できない
    {
        std::cout << dogTable[dog].jpn << std::endl;    //  配列インデックスに使用できない
    }

    return 0;
}
ナナ
ナナ

スコープなしの「enum」を利用すれば、C言語とほぼ同じ列挙型が利用できるため、それでこの問題を回避することは可能です。

もうひとつが「enum class」を整数型として扱うためのギミックを使う方法です。次はそれを紹介しましょう。

スポンサー

「enum class」を整数型として扱う方法

「enum class」は整数を扱うものの、int型のような整数型とは代入・比較などができません。

しかし、キャストを行うことで整数型に変換することは可能です。

「enum class」に対してキャストを利用した整数型変換

それでは列挙子を整数型へキャストしたプログラムを紹介しましょう。

enum class E_Dog
{
    Poodle,     //  0
    Shiba,      //  1
    Chihuahua,  //  2
    Bulldog,    //  3
};

int main()
{
    int num[4] = { 0 };

    num[(int)E_Dog::Poodle] = 100;              //  従来のキャスト

    num[static_cast<int>(E_Dog::Shiba)] = 200;  //  static_castによるキャスト

    return 0;
}

このようにC言語から利用できる従来のキャストと、C++から導入された「static_cast」を利用しても整数として扱うことが可能になります。

ナナ
ナナ

C++では、従来型のキャストよりも新しく導入された形式のキャストを推奨しております。

「static_cast」の使い方を覚えておくとよいでしょう。

static_castを利用した「enum class」のテーブルデータ参照

static_castを利用して整数型へ変換すれば、「enum class」を使ったテーブルデータの参照も可能となります。

#include <iostream>

enum class E_Dog
{
    Poodle,     //  0
    Shiba,      //  1
    Chihuahua,  //  2
    Bulldog,    //  3
};

typedef struct
{
    char jpn[32];
    char eng[32];
} S_DOG;

const S_DOG dogTable[] =
{
    {"プードル",   "Poodle" },
    {"柴犬",       "Shiba" },
    {"チワワ",     "Chihuahua" },
    {"ブルドッグ", "Bulldog" },
};

int main()
{
    for (int i = static_cast<int>(E_Dog::Poodle); i <= static_cast<int>(E_Dog::Bulldog); i++)
    {
        std::cout << dogTable[i].jpn << std::endl;
    }
    return 0;
}
スポンサー

C++の「enum」とC言語の「enum」の違い

それでは最後にC言語とC++の「enum」の違いを説明しておきましょう。

C++の「enum」はtypedefする必要がない

C言語の「enum」は列挙型を定義する際に「typedef」キーワードを利用して別名定義するのが一般的です。

C++の「enum」では「typedef」を利用しなくても型名を使って変数定義が可能です。

typedef enum 
{
    Poodle,
    Shiba,
    Chihuahua,
    Bulldog,
} E_Dog;

int main(void)
{
    E_Dog dog = Poodle;

    return 0;
}
enum E_Dog
{
    Poodle,
    Shiba,
    Chihuahua,
    Bulldog,
};

int main()
{
    E_Dog dog = Poodle;

    return 0;
}

C++の「enum」はスコープ解決演算子も利用可能

C++の「enum」は型名を指定しても、しなくても列挙子の使用が可能です。

enum E_Dog
{
    Poodle,
    Shiba,
    Chihuahua,
    Bulldog,
};

int main()
{
    E_Dog dog1 = Poodle;         // 型名なし
    E_Dog dog2 = E_Dog::Poodle;  // 型名付き

    return 0;
}

データ型の指定もできる

「enum class」と同様に列挙型に割り付けるデータ型を指定することが可能です。

enum E_Dog : long
{
    Poodle,
    Shiba,
    Chihuahua,
    Bulldog,
};

int main()
{
    E_Dog dog = Poodle;

    return 0;
}
ナナ
ナナ

C++における「enum」と「enum class」の使い方がわかりましたか?

スポンサー
C++機能解説
スポンサー
モノづくりC言語塾