2015年10月20日火曜日

【読書ノート】Effective Modern C++ - Item 10

O'Reilly Early Releaseで購入したEffective Modern C++のドラフト版の読書記録です。
※この記事はあくまで私の備忘録です。(断りなく自分の解釈や考え・感想を入れたり、理由もなく内容を省略したり、間違いや曲解もあると思います。誤りの指摘は歓迎です。)

Item 10: スコープ無しenumよりもスコープ付きenumを使おう

scoped enumではスコープが有効

一般的な規則では"{"内の変数名はそのスコープ内でしか使えないが、C++98のenumはそのenumがあるスコープ内全体で使えてしまう

enum Color { black, white, red };
auto white = false; // エラー (スコープ内で既に同じ名前が宣言されているため)

C++11のscoped enumなら上述のような「リーク」が起こらない。

enum class Color { black, white, red };
auto white = false; // OK
Color c = white; // エラー (このスコープからは見えない)
Color c = Color::white; // OK
auto c = Color::white; // OK

その宣言の仕方から"enum class"とも呼ばれる。

scoped enumなら意図しない暗黙の型変換を防げる

unscoped enumは数値型に暗黙の型変換がされる。

enum Color {black, white, red};
std::vector<std::size_t> primeFactors(std::size_t x);
Color c = red;
if (c < 14.5) { // doubleと比較
    auto factors = primeFactors(c); // intに変換
}

scoped enumは暗黙の型変換が無い。

enum class Color {black, white, red};
std::vector<std::size_t> primeFactors(std::size_t x);
Color c = red;
if (c < 14.5) { // エラー (Colorからdoubleに変換されない)
    auto factors = primeFactors(c); // エラー (Colorからstd::size_tに変換されない)
}

scoped enumで型変換を行うにはキャストが必要。

if (static_cast<double>(c) < 14.5) {
    auto factors = primeFactors(static_cast<std::size_t>(c));
}

scoped enumは前方宣言が可能

scoped enumなら定義を記述しなくても宣言が可能

enum Color; エラー
enum class Color; // OK

C++98のunscoped enumで前方宣言ができなくなっているのは、定義を見てコンパイラがデータ型を決定しているため。 メモリを最小化するためにコンパイラがenumの定義を見て最小の型を選べるようにしていた。

enum Color { black, white, red }; // コンパイラによってはchar型を選択
enum Status { good = 0,
    failed = 1,
    incomplete = 100,
    corrupt = 200,
    indeterminate = 0xFFFFFFFF
}; // charでは表現できないのでより大きな整数型が選ばれる

前方宣言ができないならenum Statusはヘッダーファイルに書くことになる。 この時enum Statusにaudited = 500を新たに加えるとそのヘッダーファイルをインクルードしているファイルは全てリコンパイルが必要。 しかしscoped enumは前方宣言可能なので、定義を実装ファイルに分離することで無駄なリコンパイルを防げる。

scoped enumのデフォルト型はint型で、他の型にオーバーライドも可能。

enum class Status; // int型
enum class Status: std::uint32_t;
enum class Status: std::uint8_t;

もちろん定義に対しても指定可能。

enum class Status: std::uint32_t {
    good = 0,
    failed = 1,
    incomplete = 100,
    corrupt = 200,
    audited = 500,
    indeterminate = 0xFFFFFFFF
};

C++11ではunscoped enumにもこの記法が使えて、その場合は前方宣言が可能に。

enum Color: std::uint8_t; 

unscoped enumの方が便利な場面と対処法

tupleの1番目の要素を取り出したい時。

using UserInfo = std::tuple<std::string, std::string, std::size_t>;
UserInfo uInfo;
…
auto val = std::get<1>(uInfo);

unscoped enumを使えば即値の1ではなくて定数名を与えることが可能。

enum UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo;
…
auto val = std::get<uiEmail>(uInfo);

scoped enumだとくどい書き方になってしまう。

enum class UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo;
…
auto val = std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uInfo);

ただし以下のコンパイル時に結果を返す関数を定義すれば

template<typename E>
constexpr auto toUType(E enumerator) noexcept {
    return static_cast<std::underlying_type_t<E>>(enumerator);
}

もう少し短く書くことが可能。

auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);

scoped enumには今までに挙げた大きな利点があるため、この場面において少々文字を多く打つことになってもscoped enumの方を使うことに価値がある。