『モダンな C++ をはじめよう 第2版』無料サンプル | Programming Place Plus e-Book Project

トップページe-Book Project「モダンな C++ をはじめよう 第2版」


このページでは、『モダンな C++ をはじめよう 第2版』の無料サンプル版を公開しています。

第1章 列挙型の強化

この章では、列挙型 (enum) に関係する新機能を紹介します。

列挙型はC言語から存在している機能ですが、C++ (C++98 の時点から)では幾らかの改良点があります。

C++ では、整数型から列挙型への暗黙的な型変換ができないように改良がなされています。 暗黙的に型変換されてしまうと、該当する値をもった列挙子が無い場合でも、誤って列挙型の変数に代入してしまうかも知れません。

相変わらず、列挙型から整数型へは暗黙的に型変換できますが、この章で紹介する C++11 の新しい列挙型(Scoped Enum)によって、この点も解決します。

また、列挙型の変数を宣言する際の「enum」を省略することができます。

enum Color {
    Black,
    White,
    Red,
    Green,
    Blue
};

int main()
{
    Color color = 1;  // コンパイルエラー。キャストが必要 (C言語では可能だった)
                      // なお「enum」は省略できるようになった。
    int v = Blue;     // キャスト不要
}

末尾のカンマ付加を許可 (C++11)

機能テストマクロ

なし

コンパイラの対応状況

  • VisualStudio 2012/2013/2015/2017
  • gcc 4.7.3~
  • clang 3.3~

解説

C++11 から、列挙型の定義の際に書き並べる列挙子の終わりに、余分なカンマ(,) を置くことが許されるようになりました。

enum Color {
    Black,
    White,
    Red,
    Green,
    Blue,    // 最後にカンマを置いても良い
};

int main() {}

この機能は、C言語では 1999年の C99規格から対応されているので、コンパイラによっては随分前から対応されています。

小粒な変更点ではありますが、列挙子1つを1行で表記するスタイルを採っていれば、列挙子の追加や削除の作業がやりやすくなる便利さがあります。

例えば、先ほどのサンプルで、列挙型の最後尾に Yellow という新たな列挙子を追加することを考えてみます。Blue の末尾にカンマが無かったとしたら、カンマを付け加えた上で Yellow の行を足す必要があります。意外とカンマの付け忘れはありがちで、付け忘れるとコンパイルエラーになってしまいます。簡単に直せることではあるものの、些細なことであるだけに、ちょっとしたストレスではあります。

また、末尾の列挙子を削除する際にも、カンマのことを気にせず、ばっさりと行ごと削除するだけで済みます。「Blue,」の行を削除したら、新たに最後尾になるのは「Green,」ですが、カンマがあっても許されるので問題ありません。以前のルールでは、末尾の「Blue」を削除した上で、Green の末尾のカンマも、忘れずに取り除かなければなりません。

移行ガイド

ごく単純な話なので、ここは省略します。

Scoped Enum (C++11)

機能テストマクロ

なし

コンパイラの対応状況

  • VisualStudio 2012/2013/2015/2017
  • gcc 4.7.3~
  • clang 3.3~

解説

C++11 で、従来の列挙型のほかに、Scoped Enum (スコープ付きの列挙型)が追加されました。対比として、従来の列挙型は、Unscoped Enum と呼ばれます。

Scoped Enum は、次のように定義します。

enum class BloodType {
    A,
    B,
    O,
    AB,
};

int main() {}

enumキーワードに続けて、class(あるいは struct)を置くことで、Scoped Enum になります。class と struct はどちらを使っても、まったく同じ意味になります。

スコープ付きであることの意味

Scoped Enum の列挙子を使用する際には、必ず列挙型の名前による修飾が必要になります。 先ほどの例で言えば、列挙子 A を「A」のように使うことはできず、常に「BloodType::A」のように指定します。このように、列挙子が列挙型のスコープに入ることで、列挙子の名前が、他の何かの名前と衝突することを防ぎます。

enum class BloodType {
    A,
    AB,
    B,
    O,
};

enum class Rank {
    A,  // BloodType::A とは区別されるので OK
    B,  // BloodType::B とは区別されるので OK
    C,
    D,
};

int main() {}

Unscoped Enum では、異なる enum に属していても、同じ名前は使えません。

enum BloodType {
    A,
    AB,
    B,
    O,
};

enum Rank {
    A,  // A という名前が多重定義となりコンパイルエラー
    B,  // B という名前が多重定義となりコンパイルエラー
    C,
    D,
};

int main() {}

暗黙的な型変換について

Scoped Enum の列挙子と整数型との間では、暗黙的な型変換は起こらずコンパイルエラーになります。変換が必要な場合には static_cast を使います。

enum class Rank {
    A,
    B,
    C,
    D,
};

int main()
{
    Rank rank = Rank::D;          // OK

    rank = 2;                     // コンパイルエラー
    rank = static_cast<Rank>(2);  // OK

    int r;
    r = rank;                     // コンパイルエラー
    r = static_cast<int>(rank);   // OK
}

Unscoped Enum では、列挙子を整数型へ暗黙的に変換できてしまいます。これは章の冒頭で取り上げた通りです。

移行ガイド

Unscoped Enum は使わないようにして、Scoped Enum を使うようにしましょう。

before (C++98/03)

従来の enum(Unscoped Enum)を使った例です。

enum BloodType {
    BloodTypeA,
    BloodTypeAB,
    BloodTypeB,
    BloodTypeO
};

enum Rank {
    RankA,
    RankB,
    RankC,
    RankD
};

void SetWeight(int w) {}

int main()
{
    int rank = RankB;               // 整数型へ暗黙的に変換されてしまう

    BloodType btype = BloodTypeAB;
    if (btype == 7) {}              // あり得ない整数値とでも比較できてしまう
    if (btype == RankB) {}          // 別の列挙型の列挙子を比較できてしまう

    SetWeight(BloodTypeB);          // 整数型へ暗黙的に変換されてしまう
}

従来の enum では、列挙子の名前が衝突することを防ぐために、プリフィックスを付けるか、それぞれ個別の namespace に入れるかする必要があります。また、どの列挙型の列挙子であるかに関係なく比較が行えてしまったり、整数型へ暗黙的に変換されてしまったりするため、至る所に危険が潜んでいます。

after (C++11/14/17)

Scoped Enum を使います。

enum class BloodType {
    A,
    AB,
    B,
    O,
};

enum class Rank {
    A,
    B,
    C,
    D,
};

void SetWeight(int w) {}

int main()
{
    int rank = Rank::B;             // コンパイルエラー。整数型へは暗黙的に変換されない

    BloodType btype = BloodType::AB;
    if (btype == 7) {}              // コンパイルエラー。整数型とは比較できない
    if (btype == Rank::B) {}        // コンパイルエラー。別の列挙型の列挙子とは比較できない

    SetWeight(BloodType::B);        // コンパイルエラー。整数型へは暗黙的に変換されない
}

誤っている可能性があった箇所はすべて、コンパイルエラーとして報告されるようになりました。

基盤型の指定 (C++11)

機能テストマクロ

なし

コンパイラの対応状況

  • VisualStudio 2012/2013/2015/2017
  • gcc 4.7.3~
  • clang 3.3~

解説

列挙子の値を表現するための「内部的な」型のことを、underlying type といいます。訳語は文献によって様々ですが、本書では基盤型としておきます。基盤型は、符号付き、あるいは符号無しの整数型です。

C++11以降では、基盤型として使う整数型を指定できるようになりました。基盤型を指定するには、列挙型を定義する際に、列挙型名の後ろに「: 基盤型の名前」のように記述します。

#include <iostream>

enum class Color : char {  // 基盤型は char
    Black,
    White,
    Red,
    Green,
    Blue,
};

enum Color2 : char {  // 基盤型は char
    Black,
    White,
    Red,
    Green,
    Blue,
};

int main()
{
    std::cout << sizeof(Color) << std::endl;
    std::cout << sizeof(Color2) << std::endl;
}

実行結果:

1
1

このサンプルプログラムの場合、基盤型は char型です。なお、列挙型に対する sizeof が返すサイズは、基盤型のサイズです。

基盤型を指定しなかったときは、Scoped Enum の場合は int型になります。Unscoped Enum の場合は、すべての列挙子を表現できる型をコンパイラが選択する ことになっています。

基盤型を指定するかどうかは、その必要性の有無によって判断すれば良いでしょう。例えば、Scoped Enum において、列挙子に int型では表現しきれない巨大な値が必要になる場合には、第3章で取り上げる long long型のような、巨大な整数型を指定することで対応できます。

また、この後の項で解説する「列挙型の宣言」という機能を Unscoped Enum で行う際には、基盤型を指定しなければなりません。ただし、Unscoped Enum よりも Scoped Enum を使うべきであることは、前の項で取り上げた通りです。

基盤型を指定する価値があるもう1つの場面として、先ほどのサンプルプログラムで char型を指定したように、小さな整数型を使うことによって、メモリ使用量を抑えるというものがあります。

基盤型を取得する

Scoped Enum の値は、std::cout で直接的に出力することができません。暗黙的に整数型へ変換されることもないので、static_cast を使って、基盤型に変換する必要があります。

C++14 では、type_traits という標準ヘッダにある std::underlying_type_t を使って、基盤型を取得することができます。std::underlying_type_t には、テンプレート実引数を1つ指定します。指定するものは、列挙型の型名です。

なお、この方法は、VisualStudio は 2013 から、gcc は 4.9 から、clang は 3.4 から使用できます。

#include <iostream>
#include <type_traits>

enum class Color : unsigned short {
    Black,
    White,
    Red,
    Green,
    Blue,
};

int main()
{
    Color color = Color::Red;

    typedef std::underlying_type_t<Color> ColorUnderlying_t;
    ColorUnderlying_t c = static_cast<ColorUnderlying_t>(color);

    std::cout << c << std::endl;
}

実行結果:

2

C++11 の時点でも、std::underlying_type を使う方法があります。こちらは、std::underlying_type<Color>::type のような、やや長い形で使用する必要があります。

#include <iostream>
#include <type_traits>

enum class Color : unsigned short {
    Black,
    White,
    Red,
    Green,
    Blue,
};

int main()
{
    Color color = Color::Red;

    typedef std::underlying_type<Color>::type ColorUnderlying_t;
    ColorUnderlying_t c = static_cast<ColorUnderlying_t>(color);

    std::cout << c << std::endl;
}

実行結果:

2

std::underlying_type_t は、std::underlying_type<>::type に対するエイリアステンプレート(第8章)です。

移行ガイド

C++98/03 には、基盤型に変わる直接的な方法はありません。

列挙型変数が使うメモリの量を減らすために、あえて char型などの小さな整数型の定数を使う手を取ることがあったかも知れません。

列挙型の宣言 (C++11)

機能テストマクロ

なし

コンパイラの対応状況

  • VisualStudio 2012/2013/2015/2017
  • gcc 4.7.3~
  • clang 3.3~

解説

C++11 からは、列挙型の宣言だけを行うことができるようになりました。

列挙型の宣言は、定義を行う構文から、列挙子を記述する部分を省略した形で行います。基盤型を指定する場合は、宣言と定義の両方で同じ型を指定するようにしなければなりません。

// 以下は宣言
enum class E1;
enum class E2 : char;
enum E3;              // エラー。Unscoped Enum では基盤型の指定が必要
enum E4 : char;

int main() {}

// 以下は定義
enum class E1 { A };
enum class E2 : char { B };  // 基盤型は宣言と揃えること
enum E3 { C };
enum E4 : char { D };  // 基盤型は宣言と揃えること

コメントで示しているように、Unscoped Enum の場合は、基盤型を指定しなければ宣言を行うことができません。 これは、宣言を行うためには、その型のサイズを決定できなければならないからです。Unscoped Enum で基盤型を指定しない場合、すべての列挙子を表現できるサイズの型を自動選択する仕様になっていますが、宣言には列挙子が書かれていないので、必要十分なサイズが判断できません。

ただし、VisualStudio では、基盤型の指定の有無に関わらず、Unscoped Enum で列挙型の宣言が行えるようです。

移行ガイド

C++ には以前から、クラス型を使うとき、メンバへのアクセスが無いのなら、そのクラスの定義が書かれたヘッダファイルをインクルードするのではなく、「class X;」のような前方宣言だけで済ませることで、依存関係を減らす方法があります。列挙型の宣言はこれと同じ価値を持ちます。列挙子を使わないのであれば、宣言だけあれば十分です。

before (C++98/03)

// color.h

#ifndef COLOR_H_INCLUDED
#define COLOR_H_INCLUDED

enum Color {
    Black,
    White,
    Red,
    Green,
    Blue
};

#endif
// line.h

#ifndef LINE_H_INCLUDED
#define LINE_H_INCLUDED

#include "color.h"  // インクルードが必要

class Line {
public:
    explicit Line(Color color);

private:
    Color  mColor;
};

#endif
// line.cpp

#include "line.h"

Line::Line(Color color) : mColor(color)
{
}
// main.cpp

#include "line.h"

int main()
{
    Line line(Red);
}

line.h には、Lineクラスのコンストラクタの引数と、メンバ変数に Color型が登場しています。Color は color.h で定義されている列挙型なので、line.h から color.h をインクルードする必要があります。そのため、Color に新しい色が追加されるなどの変更が加わるたびに、line.h をインクルードしているすべてのソースファイルが影響を受け、再コンパイルが必要になります。

after (C++11/14/17)

列挙子を使わない場合は、宣言だけで済ませるようにしましょう。また、Unscoped Enum は Scoped Enum に直します。

// color.h

#ifndef COLOR_H_INCLUDED
#define COLOR_H_INCLUDED

enum class Color {  // Scoped Enum を使うべき
    Black,
    White,
    Red,
    Green,
    Blue,
};

#endif
// line.h

#ifndef LINE_H_INCLUDED
#define LINE_H_INCLUDED

enum class Color;  // 宣言だけで良い

class Line {
public:
    explicit Line(Color color);

private:
    Color  mColor;
};

#endif
// line.cpp

#include "line.h"

Line::Line(Color color) : mColor(color)
{
}
// main.cpp

#include "color.h"  // 列挙子を使うためインクルードが必要
#include "line.h"

int main()
{
    Line line(Color::Red);  // 列挙子を使う
}

line.h や line.cpp では、Color の列挙子は使わないため、定義は必要なく、color.h をインクルードしなくて良いです。インクルードは、実際に列挙子を使う main.cpp だけで行えばよくなりました。





この本の紹介ページへ

Programming Place e-Book Project のトップページへ

Programming Place Plus のトップページへ



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