列挙型 | Programming Place Plus 新C++編

トップページ新C++編

このページの概要

このページでは、列挙型を取り上げます。列挙型は、何かしらの関係性があるいくつかの整数定数を1つの型で定義します。整数定数にはそれぞれ名前を付けることができ、それぞれを同じ型の仲間として取り扱えるようになります。名前付きの整数定数なら constexpr変数でも実現できるので、必須とまではいえない機能ですが、意外と便利なものです。

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



列挙型

前のページまでで蔵書リストのプログラムに関する話題は終了し、ここからは新しいテーマを設定して進めていきます。

次のテーマは、トランプを使ったゲーム「ポーカー」です。ポーカーにはいくつか異なるルールもありますが、ここでは、以下のようにします。

トランプを使ったゲームを作るにあたって、まずカードを表現できるようにしなければなりません。ポーカーに限らず、どんなトランプゲームにでも流用できるかたちで整えることを考えてみましょう。カードをグラフィカルに表示できると見栄えもいいのですが、C++ の標準機能だけでは画像の表示が行えないので、引き続き、文字ベースの方法を採ることにします。

カードに必要な情報は、数字とマークです(ジョーカーは省いています)。数字は int型の変数で簡単に扱えます。マークについては、“spade” のような文字列で表せますが、ここでは列挙型 (enumeration type) を使ってみることにします。

列挙体 (enumeration) と呼ばれることもあります。

列挙型は、名前の付いた整数定数を任意の個数だけ定義し、それぞれを同一の型で取り扱えるようにします。カードのマークでいえば、スペード、クラブ、ダイヤ、ハートに 0~3 の整数定数を割り当て、それぞれを CardMark型として取り扱えます。

列挙型を使うためには、構造体のときと同じく、まずは型の定義から始めます。列挙型を定義する方法は以下のように複数あります。

enum class 列挙名 {
    列挙子のリスト
};

enum struct 列挙名 {
    列挙子のリスト
};

enum 列挙名 {
    列挙子のリスト
};

このうち最初の2つは同じ意味です(新C++編では enum class の方で統一していきます)。この2つの方法で定義される列挙型は、scoped enum(スコープ付きの列挙型)と呼ばれており、C++11 から追加された新しい方法です。3つ目の記法は、古いバージョンの C++ から存在するものですが、仕様上いくつか危険な面があるため、scoped enum を使うことを勧めます。対比のため、3つ目の記法は unscoped enum と呼ばれます。

【C言語プログラマー】unscoped enum はC言語の enum を受け継いだものですが、scoped enum の機能の一部が加えられています。

列挙名 (enum name)」は、列挙型の名前のことです。「列挙子のリスト」には、この列挙型に含まれる定数の名前を , で区切りながら記述します。1つ1つの定数を列挙子 (enumerator) と呼びます。

【C言語プログラマー】C言語の列挙型タグのような考え方はなく、すべて列挙名と呼びます。列挙型の名前を使うとき、enum キーワードを使う必要もなくなっています。

カードのマークをあらわす列挙型を次のように記述できます。

// scoped enum の場合
enum class CardMark {
    spade,
    club,
    diamond,
    heart,
};

// unscoped enum の場合
enum CardMark {
    spade,
    club,
    diamond,
    heart,
}

列挙子を1行に1つずつ書くスタイルが多いですが、横に並べても構いません。また、最後の列挙子のうしろに , があってもなくても構いません。

列挙子の名前をすべて大文字で書くスタイルを見かける場合がありますが、避けるべきとするガイドラインもあります1

列挙子の値は、先頭の列挙子が 0、そのあとは +1 ずつ割り振られていきます。そのため、spade は 0、club は 1、diamond は 2、heart は 3 になります。このように自動的に採番されるため、具体的な値にはこれといって意味がない場合に便利です

列挙子の値を、自動採番に任せず、任意に指定したい場合は、次のように = を使います。= の右側には、整数になる定数式を記述します。

enum class CardMark {
    spade = 100,
    club,
    diamond,
    heart,
};

この場合は、spade が 100、それ以降は +1 されていきます。したがって、club は 101、diamond は 102、heart は 103 です。

値を明示する列挙子と、そうでない列挙子がどのように混在しても構いませんし、先に記述した列挙子を後続で使うこともできます。意味があるとは思えませんが、たとえば次のようにできます。

enum class CardMark {
    spade = 10,            // 10
    club,                  // 11
    diamond = spade + 100, // 110
    heart,                 // 111
};

値を指定すると、どの列挙子にどんな値が割り振られているのか把握しづらくなり、誤って、同じ値を持った列挙子を作ってしまうなどの事故を起こす可能性が高まります。それなりの事情がない限り、値を指定することは避けるのが無難です。前述したように、列挙型は「具体的な値にはこれといって意味がない場合」に向いています。

なお、列挙型の変数を宣言するとき、初期化子{} とした場合の初期値は 0 であるため、列挙子の中には値が 0 のものを含めておくと良いです。

列挙型と constexpr変数

列挙子は定数式として使用できます。これは constexpr変数を使っても実質同じことができるということです。たとえば、列挙型 CardMark の定義は、次のようにも書けます。

constexpr int spade = 0;
constexpr int club = 1;
constexpr int diamond = 2;
constexpr int heart = 3;

この方法では、4つの定数に関連性があるかどうかが読み取れません。型の別名を定義して、それぞれに同一の型名を与えればそれぞれが関連性を持った定数であることが分かるようになりますが、この4つ以外にも同じ仲間の定数が存在するかどうかまでは分かりません。列挙型としてまとめていれば、関連性を持った定数であることが明確であり、かつ書き並べられた列挙子以外には仲間がいないことも表せています。

したがって、関連性のある複数の定数を書き並べる(列挙する)という目的では列挙型が向いています。単独の定数を定義する場合は、constexpr変数を使えばいいでしょう。

【上級】unscoped enum では、列挙型に名前を付けない無名列挙型 (unnamed enumration) が可能です。型名がないため、共通の型を与えていることにならず、単に整数定数の定義を一か所に集めているだけに過ぎません。この方法を enumハックと呼んで活用した時代がありましたが、C++11 以降であれば、constexpr変数を使って適切な型を与えた方がいいでしょう。2

列挙型によって定数の型が明確になることには、可読性を向上させ、バグを防ぐ効果があります。

void f1(int mark);       // 引数には渡す整数がどんなものが不明瞭
void f2(CardMark mark);  // 引数に渡すものが明確
CardMark f3(void);       // 返されるものが明確

引数が CardMark型なのであれば、CardMark の列挙子のいずれかを選んで渡さなければならないことが明確になり、間違っていれば、コンパイラがエラーを出すことができます。

基底型

列挙子の値として使える整数の範囲は、基底型(根底型) (underlying type) という型によって決定されます。基底型は自動的に決定されますが、次のように明示的に指定することもできます。

enum class 列挙名 : 基底型 {
    列挙子のリスト
};

enum struct 列挙名 : 基底型 {
    列挙子のリスト
};

enum 列挙名 : 基底型 {
    列挙子のリスト
};

「基底型」のところに int とか unsigned int といったように、任意の整数型を記入します。int型を指定したのなら、列挙子の値として使える範囲は int型で表現できる範囲と同じになります。unsigned int型を指定したなら unsigned int型と同じになるので、正の上限値が増える代わりに負数が使えなくなります。まだ登場していませんが、整数型には、int型よりも大きいものと小さいものとがそれぞれ存在するので、これらを使い分けることもできます。

char型や bool型も整数型の一種なので使用できます。ただ、char型は文字として使うべき型ですし、bool型には true/false の2つの値しか存在しないため、基底型として使うことはないでしょう。

基底型の指定を行わなかった場合のあつかいは、scoped enum と unscoped enum とで異なります3

unscoped enum の場合、処理系定義の選択に任せず、明示的に指定したほうがいいかもしれません。

【上級】scoped enum で、int型で表現できない値を使うには、基底型に long long型など、より大きな整数型を指定しなければなりません。unscoped enum の場合は、自動的に大きな整数型が使われますが、何が選ばれるかは処理系定義です。

scoped enum

scoped enum の CardMark型を次のように使用できます。

#include <iostream>
#include <string>

enum class CardMark {
    spade,
    club,
    diamond,
    heart,
};

std::string get_mark_string(CardMark card_mark)
{
    // switch文の caseラベルに使える
    switch (card_mark) {
    case CardMark::spade:
        return "spade";
    case CardMark::club:
        return "club";
    case CardMark::diamond:
        return "diamond";
    case CardMark::heart:
        return "heart";
    default:
        return "";
    }
}

int main()
{
    CardMark mark {CardMark::spade};  // 変数の宣言と初期化
    mark = CardMark::diamond;         // 代入
    std::cout << std::boolalpha << (mark == CardMark::diamond) << "\n";  // 比較

    std::cout << get_mark_string(mark) << "\n";
}

実行結果:

true
diamond

定義した scoped enum の列挙名を使って、これまで通りの方法で変数を宣言できます。初期化子を {} とした場合は 0 で初期化されます(「構造体」のページを参照)。初期化子を与えなかった場合の値は不定なので、これは避けましょう。

特徴的なのは列挙子へのアクセスの方法で、:: を使って「列挙名::列挙子名」のように記述しなければなりません。::スコープ解決演算子 (scope resolution operator) と呼ばれる演算子の一種です。“scoped” enum という名前はここから来ており、列挙子の名前は、列挙名とセットになっています。記述がやや面倒になる反面、ほかの列挙型に含まれる同名の列挙子と確実に区別され、衝突することがありません。

【C++20】using enum宣言を行うことで、スコープ解決演算子を用いずに列挙子を使えるようになりました4

get_mark_string関数は、列挙子の名前に応じた文字列を作って返すものです。列挙子から文字列への変換は時折必要になるのですが、自力で実装する必要があります。

整数型との型変換

scoped enum の列挙子を、整数値として計算や比較に使うことはできません。また、scoped enum から整数型へは暗黙的に変換されません。そのため、次のコードはコンパイルエラーになります。

CardMark mark {CardMark::spade};  // scoped enum (基底型は int)
mark = mark + 2;  // コンパイルエラー
if (mark == 1) {  // コンパイルエラー
}
std::cout << mark << "\n";  // コンパイルエラー

整数として扱わせるためには、static_cast でキャストする必要があります。

CardMark mark {CardMark::diamond};  // scoped enum (基底型は int)
mark = static_cast<CardMark>(static_cast<int>(mark) + 2);  // OK。だが、+2 した結果が CardMark として正常なものであると保証できるのか?
if (static_cast<int>(mark) == 1) {  // OK。だが mark == CardMark::club が適切だろう
}
std::cout << static_cast<int>(mark) << "\n";  // OK

見てのとおり、これは非常に面倒です。scoped enum の列挙子は計算処理などに使うことには向いていません。列挙型によって表現される定数は、「具体的な値にはこれといって意味がない場合」に便利なものであって、列挙子の具体的な値を期待して使うべきではありません。mark + 2 の結果が、現在のマークより2つ後ろに定義されたマークを表すことは期待すべきではないですし、mark == 11 がクラブを意味していることを期待すべきではないです。

ここでは、static_cast の変換先の型として int を選んでいますが、これは基底型が int だと分かっているからです。scoped enum の基底型はプログラマーが判断できるので、それを指定すればいいですが、std::underlying_type_t5 6 を使ってコンパイラに判断させることもできます(後で基底型の指定を変更したときに自動対応できる利点があります)。std::underlying_type_t を使うには、#include <type_traits> が必要です。やや複雑ですが、次のように書きます。

#include <iostream>
#include <type_traits>

enum class CardMark : unsigned int {
    spade,
    club,
    diamond,
    heart,
};

int main()
{
    CardMark mark {CardMark::diamond};
    auto mark_value = static_cast<std::underlying_type_t<CardMark>>(mark);
    std::cout << mark_value << "\n";
}

実行結果:

2

std::underlying_type_t<列挙名> という記述によって、「列挙型」の基底型の名前が得られます。これを static_cast の変換後の型名として使っています。

【C++98/03 経験者】テンプレート実引数の > が2つ重なると、右シフト演算子だと判断されてしまう問題は C++11 で解消しているため、空白を入れる必要はありません。7


整数型から scoped enum へ変換する場合も、static_cast が必要です。ただしこちらは、本当にその変換が適切なものなのか注意しなければなりません。

// CardMark の基底型は unsigned int とする

mark = 3;  // コンパイルエラー
mark = static_cast<CardMark>(3);   // OK
mark = static_cast<CardMark>(7);   // OK だが危険(想定されていない値)
mark = static_cast<CardMark>(-1);  // OK だが危険(基底型で表現できない値)

7 を CardMark型に変換することは可能ですが、CardMark型には 7 という値を持つ列挙子が存在していません。CardMark型を定義したときに想定していない値ということですから、これはおそらく間違っています。同様に、基底型で表現できないような値を無理やり代入することも問題があります。

unscoped enum

今後は unscoped enum の場合の話です。最初に書いたとおり、通常は scoped enum を使うことを勧めます。

#include <iostream>
#include <string>

enum CardMark : int {
    spade,
    club,
    diamond,
    heart,
};

std::string get_mark_string(CardMark card_mark)
{
    // switch文の caseラベルに使える
    switch (card_mark) {
    case spade:
        return "spade";
    case club:
        return "club";
    case diamond:
        return "diamond";
    case heart:
        return "heart";
    default:
        return "";
    }
}

int main()
{
    CardMark mark {spade};            // 変数の宣言と初期化
    mark = diamond;                   // 代入
    std::cout << std::boolalpha << (mark == diamond) << "\n";  // 比較

    std::cout << get_mark_string(mark) << "\n";
}

実行結果:

false
diamond

unscoped enum の変数を宣言するとき、初期化子を {} とした場合は 0 で初期化されます(「構造体」のページを参照)。初期化子を与えなかった場合の値は不定なので、これは避けましょう。

unscoped enum の場合、列挙子名は列挙名とセットではなく、スコープ解決演算子(::)を使う必要がありません(使ってもいいです)。このため、unscoped enum では、ほかの unscoped enum に同名の列挙子があると衝突を起こしてエラーになります。

enum CardMark : int {
    spade,
    club,
    diamond,
    heart,
};

enum BodyParts : int {
    hands,
    legs,
    heart,  // エラー。heart はすでにほかの unscoped enum にある
    head,
};

そのため、unscoped enum を使う場合は、列挙子名の先頭に、列挙名に由来する名前を付け足して、衝突を防ぐと良いです。

enum CardMark : int {
    cardMarkSpade,
    cardMarkClub,
    cardMarkDiamond,
    cardMarkHeart,
};

enum BodyParts : int {
    bodyPartsHands,
    bodyPartsLegs,
    bodyPartsHeart,
    bodyPartsHead,
};

整数型との型変換

整数型との相互変換についても、scoped enum とはルールが異なります。

unscoped enum から整数型へは暗黙的に変換されます。変換先の型は基底型に応じて決まります。scoped enum ではできなかった以下の記述はすべて受け付けられます。

// CardMark の基底型は int とする

CardMark mark {cardMarkDiamond};
int mark_value {mark};  // OK
std::cout << mark << "\n";
mark_value = mark + 1;

【上級】unscoped enum から整数型への暗黙の型変換は、汎整数拡張が適用されます8。そのため、int や unsigned int よりも変換順位の低い型にはなりません。

このコードでは int型で受け取っていますが、基底型が int型よりも表現範囲が大きい型の場合には問題があります。scoped enum のときと同じく、std::underlying_type_t を使って、基底型に応じた型で受け取らなければなりません。

#include <iostream>
#include <type_traits>

enum CardMark : unsigned int {  // 基底型は unsigned int
    cardMarkSpade,
    cardMarkClub,
    cardMarkDiamond,
    cardMarkHeart,
};

int main()
{
    CardMark mark {cardMarkDiamond};
    std::underlying_type_t<CardMark> mark_value = mark;
    std::cout << mark_value << "\n";
}

実行結果:

2

暗黙的に変換されることは、scoped enum と比べて便利に感じるかもしれませんが、型による強制力がないということなので、良いことだとはいえません。たとえば、ほかの unscoped enum の列挙子を誤って使ってしまう恐れがあります。

if (mark == bodyPartsHead) {  // CardMark型と BodyParts型を誤って比較。
                              // それぞれが整数型に変換されて、比較される。本来比較できるべきではない。
}

一方、整数型から unscoped enum への変換には、scoped enum と同様に、static_cast が必要です。列挙型が想定していない値や、基底型で表現できない値を無理やり変換することは可能ですが、危険です。

// CardMark の基底型は unsigned int とする

mark = 3;  // コンパイルエラー
mark = static_cast<CardMark>(3);   // OK
mark = static_cast<CardMark>(7);   // OK だが危険(想定されていない値)
mark = static_cast<CardMark>(-1);  // OK だが危険(基底型で表現できない値)

enum宣言

列挙型を宣言しておくこともできます。宣言には列挙子の指定が含まれず、列挙名と基底型だけを記述します。

enum class 列挙名;
enum struct 列挙名;

enum class 列挙名 : 基底型;
enum struct 列挙名 : 基底型;
enum 列挙名 : 基底型;

scoped enum の場合は「基底型」を省略できますが、unscoped enum では「基底型」を記述しなければなりません。これは、列挙子を書かないため、基底型が分かっていないと、必要な大きさが分からないためです(scoped enum は基底型を指示しなければ int型というルールがあるため、省略可能)。

宣言には列挙子が含まれていないので、どこかに定義も必要です。宣言と定義で、列挙名と基底型が一致していなければなりません。

宣言さえ見えていれば、列挙名を使えますが、列挙子の名前を使うには定義が見えていなければなりません。

#include <iostream>

// 宣言
enum CardMark : unsigned int;

void f(CardMark mark);  // OK。CardMark の宣言が上にあるので、CardMark という名前を使える

int main()
{
    f(CardMark::spade);  // エラー。この位置からは列挙子が見えてない
}

// 定義
enum CardMark : unsigned int {
    spade,
    club,
    diamond,
    heart,
};

void f(CardMark mark)
{
    if (mark == CardMark::spade)  // OK。この位置からは定義が見えるので、列挙子も使える
    {
        // ...
    }
}

現時点では、enum の宣言を活用できる場面はありませんが、ソースファイルを複数使うようになってくると必要性が出てきます。

この話題は「ヘッダファイル」のページで取り上げます。

まとめ


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク


練習問題

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

次の列挙型で、列挙子の値はそれぞれいくつですか?

enum class Fruit {
    apple,
    banana,
    melon = 10,
    orange,
    strawberry = -10,
    watermelon,
};

解答・解説

問題2 (確認★)

次の中から、エラーになるものをすべて選んでください。

  1. scoped enum型の変数に 10 を代入する
  2. scoped enum と int型の値を比較する
  3. unscoped enum型の変数を、その基底型の変数に代入する
  4. int型の値を unscoped enum型の変数に代入する
  5. scoped enum と unscoped enum を比較する

解答・解説

問題3 (基本★★)

トランプのマークを表現する列挙型の値を渡すと、その色を返す関数を作成してください。色についても列挙型で表現するようにしてください。

マークと色の関係は、スペードとクラブが黒、ダイヤとハートが赤とします。

解答・解説

問題4 (応用★★★)

次のような関数は分かりづらいといえます。列挙型を使って改善してください。

// 物体の位置を移動させる。
// pos: 元の位置
// isUp: true を指定すると上へ、false を指定すると下へ移動する
// isFast: true を指定すると高速に移動、false を指定すると通常の速度で移動する
// 戻り値: 移動後の位置
Position move(const Position& pos, bool isUp, bool isFast);

解答・解説


解答・解説ページの先頭



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る