スコープ付き列挙型[C++11]
列挙型(enum)はC言語ですでに使えるようになっていた言語仕様で、マジックナンバー(直にコーディングされた数値など)を嫌うプログラマが好んで使っていたように思えます(筆者です)。なんてことのない定数値でも、わざわざ名前を割り当てて使うという感じです。「enum Boolean {False = 0, True}」みたいな感じですね。
従来からの列挙型
とはいえ、マジックナンバーはコードの可読性を下げますし、値と意味の重複が起きる可能性があるので、列挙型で置き換えるのは妥当ですね。例えば、以下のリストです。
enum Trump {Spade, Heart, Club, Diamond};
auto card = Spade; // 0
このとき、列挙型Trumpの各メンバには、0からの整数値が順番に割り振られます(メンバ名に「=」を続けて値を明示的に指定もできました)。なので、cardを出力させると0となります。ちなみに、トランプは英語にするとPlaying Cardsです。分かりやすいようにTrumpとしただけですので誤解しないでくださいね。
また、同じメンバを持つ列挙型、例えばTrump2を宣言しようとしても、再定義のエラーとなります。列挙型間でメンバが重複しないように、細心の注意を払う必要があるわけですね。筆者などは、各メンバに列挙型の名前をプレフィクスで付けるとか、今となっては冗長なことをしていました。
スコープ付き列挙型
そんなことで、C++ 11において「スコープ付き列挙型」(scoped enumeration type)が導入されました。スコープ付き列挙型では、列挙型名によってメンバ名がスコープされるので、列挙型間でメンバ名の重複が起きません。想像しただけですごく便利そうですね。なお、スコープ付き列挙型の導入に合わせて、従来の列挙型は「スコープのない列挙型」(unscoped enumeration type)として区別されることになりました。
スコープ付き列挙型は、以下のリストのようにenum classあるいはenum struct構文で定義します(enum classとenum structは同じ働きをします)。
enum Trump {Spade, Heart, Club, Diamond};
enum class Jewel {Garnet = 1, Amethyst, Aquamarine, Diamond};
auto birth_jewel = Jewel::Diamond;
cout << static_cast<int>(birth_jewel) << endl; // 実行結果:4
メンバDiamondはスコープのない列挙型Trumpとスコープ付き列挙型Jewelで被っていますが、再定義エラーにはなりません。その替わり、メンバにアクセスする際にはJewel::Diamondのように列挙型名でスコープ演算子(::)を使う必要があります。また、スコープのない列挙型のようにメンバに明示的な値の指定も可能です。
スコープ付き列挙型では、メンバに対して暗黙の型変換を実施しません。実際の値を取り出すには、上記リストのようにstatic_cast演算子を使っての明示的な型変換が必要です。
基底型を指定できる
スコープ付き列挙型では、各メンバのデータ型(基底型)を指定できます。基底型に指定できるのは数値型のみなので、メモリレイアウトを考慮しているということなのでしょう。基底型は、以下のリストのように列挙型名にコロン(:)を続けてそのあとに指定します。なお、スコープ付き列挙型における既定の基底型は「int」です。
enum class Jewel2: char {Garnet, Amethyst, Aquamarine, Diamond};
auto birth_jewel2 = Jewel2::Diamond;
cout << +static_cast<underlying_type<Jewel2>::type>(birth_jewel2) << endl;
この場合、birth_jewel2はchar型を基底型に持ったメンバということになります。実際の値は、既述のように静的キャストで取り出すことができますが、ここでstd::underlying_typeで実際の型を取り出して、それを指定することができます。わざわざ何度も同じ型名を書かなくて済みますし、基底型を変更したときにも楽ですね。
なお、これに合わせて従来の列挙型でも、基底型を指定できるようになりました。
イテレータと範囲for
配列をはじめとするコレクションには、必ずと言ってよいほど繰り返し処理、要素の走査といった処理が付きものです。そのため、モダンなプログラミング言語には「イテレータ」(反復子)という仕組みが備わっていて、これを簡便なものにしてくれていることがほとんどです。我らがC++言語にもイテレータが存在しましたが、それはイテレータとは似て非なるもの、という感が強かった記憶があります。このイテレータがどういうものだったのかというあたりから見ていきましょう。
イテレータ構文
C++におけるイテレータとは、コンテナの各要素を指すポインタです。例えばstd::vectorといったコンテナにおいて、その先頭要素(begin())や最終要素(end())を指すイテレータを取得し、範囲内で要素にアクセスするといった使い方が基本です。これを使って繰り返しをするのが以下のリストです。
#include <vector>
std::vector<int> x = {0, 1, 2, 3, 4};
for(auto it = x.begin(); it != x.end(); it++)
{
cout << *it << endl;
}
イテレータと称していますが、C/C++伝統のfor文を使って、地道にループを回しているだけです。分かりやすいといえば分かりやすいですね。ちなみに、イテレータはポインタであるので、間接参照の解決のための演算子(*)を値取り出しのために使って、先に進めるにはインクリメント演算子(++)を使えます。
範囲for文[C++11]
上記の構文は、古いC/C++プログラマから見れば当たり前の構文ですが、他言語のスキッとした構文を見たことがあれば、もう少し何とかならないの?となりますね。ということで、C++ 11において範囲for文(range-based for)が使えるようになりました。範囲for文を使うと、上記のリストは以下のように書き直すことができます。
for(auto& it: x)
{
cout << it << endl;
}
やりました、すごくスッキリしましたね。しかも、間接参照演算子もなくなって、イテレータがポインタであることを意識しないで済むようになりました。その替わりといっては何ですが、for文のカッコの中の書き方には、Modern C++ならではの使い分けが必要です。こんな感じになっています。
- auto& it:イテレータ自身を変更でき、コンテナ要素も変更できる
- auto it:イテレータ自身は変更できるが、コンテナ要素は変更できない
- const auto& it:イテレータ自身もコンテナ要素も変更できない
要は、参照の性質そのものです。特に、2番目の書式ではコンテナ要素の内容がコピーされるので、コピーコンストラクタが必要なことに注意してください。なお、範囲for文は、コンパイル時に従来のfor文に展開されます。そのため、コンテナの実装にはbegin()とend()が必要なことは変わらないのです。
