C++のenum classについて

AI実装検定のご案内

C++のenum classは、関連する複数の定数を、独立した型として安全に管理するための仕組みです。

正式には「スコープ付き列挙型」と呼ばれ、C++11で導入されました。

従来のenumよりも名前の衝突が起こりにくく、整数への意図しない変換も防げるため、現在のC++では基本的にenum classの使用が推奨されます。

enum class Color {
    Red,
    Green,
    Blue
};

使用するときは、列挙型名とスコープ解決演算子::を付けます。

Color color = Color::Red;

次のように、列挙子だけを書くことはできません。

Color color = Red;  // コンパイルエラー

enum classでは、列挙子が列挙型自身のスコープ内に置かれるためです。

目次

enum classの基本的な特徴

列挙子を型名付きで指定する

enum classでは、列挙子を次のように指定します。

Color::Red
Color::Green
Color::Blue

型名を付けることで、どの列挙型に属する値なのかが明確になります。

例えば、複数の列挙型に同じ名前の列挙子を定義できます。

enum class TrafficLight {
    Red,
    Yellow,
    Green
};

enum class FruitColor {
    Red,
    Yellow,
    Green
};

それぞれの値は、次のように区別されます。

TrafficLight light = TrafficLight::Red;
FruitColor color = FruitColor::Red;

TrafficLight::RedFruitColor::Redは、名前が同じでも異なる型の値です。

整数へ暗黙変換されない

enum classの値は、整数型へ自動的に変換されません。

enum class Status {
    Success,
    Error
};

Status status = Status::Error;

int value = status;  // コンパイルエラー

この制限によって、列挙型の値が意図せず数値計算に使われるのを防げます。

また、boolにも暗黙変換されません。

if (status) {  // コンパイルエラー
}

整数として扱いたい場合は、明示的な変換が必要です。

int value = static_cast<int>(status);

異なる列挙型同士を混同できない

列挙子の数値が同じでも、列挙型が異なれば別の型として扱われます。

enum class Color {
    Red = 1
};

enum class Signal {
    Stop = 1
};

次の比較はできません。

Color color = Color::Red;
Signal signal = Signal::Stop;

bool result = color == signal;  // コンパイルエラー

内部の値がどちらも1であっても、ColorSignalは異なる型です。

この型安全性によって、意味の異なる値を誤って比較したり、関数へ渡したりするミスを防げます。

通常のenumとの違い

通常のenumの特徴

従来の列挙型は、次のように書きます。

enum Color {
    Red,
    Green,
    Blue
};

列挙子は、型名を付けずに使用できます。

Color color = Red;

C++11以降では、次のように型名を付けることも可能です。

Color color = Color::Red;

ただし、通常のenumでは、RedGreenといった列挙子名が周囲のスコープにも公開されます。

enum Color {
    Red,
    Green
};

enum Signal {
    Red,  // 名前が重複するためエラー
    Yellow
};

このため、別の列挙型で同じ列挙子名を使うと、名前が衝突する可能性があります。

通常のenumは整数へ暗黙変換される

通常のenumは、整数型へ暗黙変換されます。

enum Color {
    Red,
    Green,
    Blue
};

Color color = Red;

int value = color;  // 変換可能

次のような数値計算も成立してしまいます。

int result = color + 100;

文法上は正しくても、列挙型としての意味を考えると不自然な処理になりやすい点が問題です。

enumenum classの比較

項目enumenum class
列挙子の指定RedまたはColor::Red原則としてColor::Red
列挙子のスコープ周囲のスコープにも公開される列挙型のスコープ内に限定される
整数への暗黙変換されるされない
異なる列挙型同士の比較整数変換を通じて成立する場合がある基本的にコンパイルエラー
名前の衝突起こりやすい起こりにくい
型安全性比較的低い高い
現代的なC++での使用特別な理由がある場合原則として推奨

列挙子に数値を指定する方法

数値を明示的に割り当てる

列挙子には整数値を割り当てられます。

enum class Status {
    Success = 0,
    NotFound = 404,
    ServerError = 500
};

使用方法は通常と同じです。

Status status = Status::NotFound;

数値を指定しなかった場合、最初の列挙子は通常0になり、その後は1ずつ増加します。

enum class Level {
    Low,     // 0
    Medium,  // 1
    High     // 2
};

途中の値だけ指定することもできます。

enum class Level {
    Low = 1,
    Medium,      // 2
    High = 10,
    VeryHigh     // 11
};

外部仕様で使う値は明示する

列挙子の値を次のような用途で使う場合は、数値を明示したほうが安全です。

  • データベースへ保存する
  • 通信データとして送信する
  • ファイルへ書き込む
  • 外部APIと連携する
  • バイナリ形式で扱う

例えば、次のように定義します。

enum class MessageType {
    Login = 1,
    Logout = 2,
    SendMessage = 3
};

暗黙の連番を利用していると、途中に列挙子を追加した際に、既存の数値が変わる可能性があります。

基底型とは

enum classは独立した型である

列挙型は整数型そのものではありません。

enum class Status : int {
    Success,
    Error
};

この場合、Statusintを基底型として持ちますが、Statusintは異なる型です。

そのため、次のコードは成立しません。

Status status = 1;  // コンパイルエラー

より正確には、列挙型は独立した型であり、その値の表現に整数型を利用します。

基底型を省略した場合はint

enum classで基底型を省略した場合、基底型はintになります。

enum class Status {
    Success,
    Error
};

基底型については、次の定義と同等です。

enum class Status : int {
    Success,
    Error
};

基底型を明示する

基底型は、次のように指定できます。

#include <cstdint>

enum class Status : std::uint8_t {
    Success = 0,
    Warning = 1,
    Error = 2
};

基底型には整数型を指定します。

enum class ErrorCode : int {
    None = 0,
    Failed = -1
};

enum class LargeValue : unsigned long long {
    Maximum = 10000000000ULL
};

基底型を指定する主な目的は次のとおりです。

  • 通信仕様に合わせる
  • ファイル形式に合わせる
  • バイナリ上の表現を明確にする
  • 外部APIと数値表現をそろえる
  • 表現可能な値の範囲を明確にする

ただし、基底型を指定しただけで、C言語や外部システムとの完全なABI互換性が保証されるわけではありません。

外部へ値を渡す場合は、固定幅整数型へ明示的に変換する設計が安全です。

enum classを整数へ変換する方法

static_castを使用する

enum classは整数へ暗黙変換されないため、明示的にキャストします。

enum class Status {
    Success = 0,
    Error = 1
};

Status status = Status::Error;

int value = static_cast<int>(status);

ただし、基底型が必ずintとは限りません。

std::underlying_type_tを使用する

基底型へ正確に変換する場合は、std::underlying_type_tを使用します。

#include <type_traits>
#include <cstdint>

enum class Status : std::uint8_t {
    Success = 0,
    Error = 1
};

using Underlying = std::underlying_type_t<Status>;

Underlying value =
    static_cast<Underlying>(Status::Error);

std::underlying_type_t<Status>には、Statusの基底型が設定されます。

この例ではstd::uint8_tです。

C++23ではstd::to_underlyingが使える

C++23以降では、std::to_underlyingを使用できます。

#include <utility>

enum class Status : unsigned int {
    Success = 0,
    Error = 1
};

auto value = std::to_underlying(Status::Error);

戻り値の型は、列挙型の基底型です。

汎用的なコードでは、static_cast<int>よりも安全で意図が分かりやすい方法です。

整数からenum classへ変換する方法

static_castで変換する

整数から列挙型へ変換する場合も、明示的なキャストを使います。

enum class Status {
    Success = 0,
    Warning = 1,
    Error = 2
};

int value = 1;

Status status = static_cast<Status>(value);

ただし、変換元の値に対応する列挙子が存在するとは限りません。

Status status = static_cast<Status>(100);

このコードは、基底型の範囲内であればコンパイルできます。

しかし、Status::SuccessStatus::WarningStatus::Errorのどれにも該当しません。

外部入力は変換前に検証する

ユーザー入力やファイル、通信データなど、外部から受け取った値は、そのままキャストせずに検証するのが安全です。

#include <optional>

enum class Status {
    Success = 0,
    Warning = 1,
    Error = 2
};

std::optional<Status> toStatus(int value)
{
    switch (value) {
    case 0:
        return Status::Success;

    case 1:
        return Status::Warning;

    case 2:
        return Status::Error;

    default:
        return std::nullopt;
    }
}

使用例は次のとおりです。

int input = 1;

if (auto status = toStatus(input)) {
    // 正しいStatusとして利用できる
}
else {
    // 不正な値
}

関数の引数や戻り値に使う方法

選択肢を明確に表現できる

enum classは、関数へ有限個の選択肢を渡す場合に適しています。

enum class SortOrder {
    Ascending,
    Descending
};

void sortData(SortOrder order)
{
    if (order == SortOrder::Ascending) {
        // 昇順
    }
    else {
        // 降順
    }
}

呼び出し側も、意味が分かりやすくなります。

sortData(SortOrder::Ascending);

boolで表現した場合は、呼び出しだけを見ても意味が分かりにくくなります。

void sortData(bool ascending);

sortData(true);

一方、enum classでは意図がコードに現れます。

sortData(SortOrder::Ascending);

異なる種類の値を誤って渡せない

enum class LogLevel {
    Debug,
    Info,
    Warning,
    Error
};

enum class UserRole {
    Guest,
    Member,
    Admin
};

void setLogLevel(LogLevel level)
{
}

次のコードはコンパイルエラーになります。

setLogLevel(UserRole::Admin);

UserRoleLogLevelは異なる型だからです。

switch文で使う方法

基本的な書き方

enum classは、switch文との相性がよい型です。

#include <iostream>

enum class Status {
    Success,
    Warning,
    Error
};

void printStatus(Status status)
{
    switch (status) {
    case Status::Success:
        std::cout << "成功\n";
        break;

    case Status::Warning:
        std::cout << "警告\n";
        break;

    case Status::Error:
        std::cout << "エラー\n";
        break;
    }
}

defaultを省略する場合

すべての列挙子を処理する場合、あえてdefaultを書かない方法があります。

switch (status) {
case Status::Success:
    break;

case Status::Warning:
    break;

case Status::Error:
    break;
}

後から列挙子を追加した際、適切な警告オプションを有効にしていれば、処理漏れをコンパイラが警告してくれる場合があります。

enum class Status {
    Success,
    Warning,
    Error,
    Unknown
};

defaultがあると、新しく追加したUnknowndefaultで処理され、追加漏れに気づきにくくなることがあります。

不正値を考慮する場合

列挙子として定義されていない値が入る可能性がある場合は、想定外の値を処理する必要があります。

void process(Status status)
{
    switch (status) {
    case Status::Success:
        return;

    case Status::Warning:
        return;

    case Status::Error:
        return;
    }

    // 列挙子として定義されていない値の処理
    handleInvalidStatus();
}

この書き方なら、列挙子の処理漏れに関する警告を活用しながら、不正値にも対応できます。

列挙子を文字列へ変換する方法

そのままでは列挙子名を出力できない

列挙子の名前は、自動的に文字列として保存されるわけではありません。

enum class Status {
    Success,
    Warning,
    Error
};

次のコードで、Successという文字列が出力されるわけではありません。

std::cout << Status::Success;

通常は、文字列へ変換する関数を用意します。

#include <string_view>

constexpr std::string_view toString(Status status)
{
    switch (status) {
    case Status::Success:
        return "Success";

    case Status::Warning:
        return "Warning";

    case Status::Error:
        return "Error";
    }

    return "Unknown";
}

使用例は次のとおりです。

#include <iostream>

Status status = Status::Warning;

std::cout << toString(status) << '\n';

ストリーム演算子を定義する

頻繁に出力する場合は、operator<<を定義できます。

#include <ostream>
#include <string_view>

std::ostream& operator<<(std::ostream& os, Status status)
{
    return os << toString(status);
}

これにより、次のように直接出力できます。

Status status = Status::Error;

std::cout << status << '\n';

クラス内で定義する方法

クラスに関連する列挙型をまとめられる

列挙型が特定のクラスだけに関係する場合は、クラス内に定義できます。

class NetworkClient {
public:
    enum class State {
        Disconnected,
        Connecting,
        Connected,
        Error
    };

    State getState() const
    {
        return state_;
    }

private:
    State state_ = State::Disconnected;
};

クラス外から使用する場合は、次のように書きます。

NetworkClient::State state =
    NetworkClient::State::Connected;

NetworkClient::State::Connectedは、次の意味を表しています。

  • NetworkClientクラスの
  • State列挙型に属する
  • Connectedという値

関連する型をクラス内へまとめることで、責務や所属が明確になります。

前方宣言する方法

列挙子を定義する前に型を宣言できる

enum classは、列挙子の一覧を定義する前に宣言できます。

enum class Status;

その後で、列挙子を定義します。

enum class Status {
    Success,
    Error
};

ただし、クラスの前方宣言とは少し性質が異なります。

class User;

クラスは定義されるまで不完全型ですが、次の列挙型は宣言した時点で完全型になります。

enum class Status;

基底型がintに固定されるため、サイズを決定できるからです。

enum class Status;

static_assert(sizeof(Status) == sizeof(int));

Status status{};

ただし、列挙子はまだ定義されていないため、次のコードは使用できません。

enum class Status;

Status status = Status::Success;  // コンパイルエラー

基底型を指定して宣言する

明示的な基底型を使う場合は、宣言と定義で同じ型を指定します。

#include <cstdint>

enum class Status : std::uint8_t;

enum class Status : std::uint8_t {
    Success,
    Error
};

宣言と定義で基底型を変えることはできません。

enum class Status : std::uint8_t;

enum class Status : int {  // コンパイルエラー
    Success,
    Error
};

C++20のusing enum

列挙型名を省略できる

C++20では、using enumを使って列挙子を現在のスコープへ導入できます。

enum class Color {
    Red,
    Green,
    Blue
};

void printColor(Color color)
{
    using enum Color;

    switch (color) {
    case Red:
        break;

    case Green:
        break;

    case Blue:
        break;
    }
}

通常は次のように書きます。

case Color::Red:
case Color::Green:
case Color::Blue:

using enum Color;を使うと、そのスコープ内ではColor::を省略できます。

ただし、広いスコープで使用すると名前の衝突が起こりやすくなります。

関数内や小さなブロック内に限定して使用するのが安全です。

ビットフラグとして使用する方法

フラグ用の値を定義する

enum classは、複数の状態を組み合わせるビットフラグにも利用できます。

enum class Permission : unsigned int {
    None    = 0,
    Read    = 1u << 0,
    Write   = 1u << 1,
    Execute = 1u << 2
};

それぞれの値は、次のビットを表します。

None    = 000
Read    = 001
Write   = 010
Execute = 100

ビット演算子を定義する

enum classでは、ビットOR演算子は自動的に使えません。

Permission permission =
    Permission::Read | Permission::Write;  // そのままではエラー

演算子を定義する必要があります。

#include <type_traits>

constexpr Permission operator|(
    Permission left,
    Permission right)
{
    using U = std::underlying_type_t<Permission>;

    return static_cast<Permission>(
        static_cast<U>(left) |
        static_cast<U>(right)
    );
}

これにより、複数の権限を組み合わせられます。

Permission permission =
    Permission::Read | Permission::Write;

フラグを持っているか確認する

特定のフラグを持っているか確認する関数は、次のように定義できます。

constexpr bool hasPermission(
    Permission permissions,
    Permission target)
{
    using U = std::underlying_type_t<Permission>;

    return (
        static_cast<U>(permissions) &
        static_cast<U>(target)
    ) != 0;
}

使用例は次のとおりです。

Permission permissions =
    Permission::Read | Permission::Write;

if (hasPermission(permissions, Permission::Read)) {
    // 読み取り権限がある
}

この関数は、「指定したフラグのうち、どれか1つが含まれているか」を判定します。

複数のフラグがすべて含まれているか確認する場合は、次のようにします。

constexpr bool hasAllPermissions(
    Permission permissions,
    Permission required)
{
    using U = std::underlying_type_t<Permission>;

    const U value = static_cast<U>(permissions);
    const U mask = static_cast<U>(required);

    return (value & mask) == mask;
}

なお、Permission::Noneは値が0なので、ビットANDによる判定には向きません。

if (permissions == Permission::None) {
    // 権限がない
}

大きなビット位置では型を明示する

次のコードでは、1int型です。

1 << 40

一般的な32ビットのintでは、40ビットのシフトを正しく扱えません。

大きなビット位置を使う場合は、基底型に合わせます。

#include <cstdint>

enum class Permission : std::uint64_t {
    None  = 0,
    Read  = std::uint64_t{1} << 0,
    Write = std::uint64_t{1} << 1,
    Large = std::uint64_t{1} << 40
};

列挙子の値は重複できる

別名として同じ値を設定できる

複数の列挙子に同じ値を設定できます。

enum class Result {
    Success = 0,
    Ok = 0,
    Error = 1
};

SuccessOkは異なる名前ですが、値としては同じです。

static_assert(Result::Success == Result::Ok);

ただし、switch文では同じ値を持つ列挙子を別々のcaseとして書けません。

switch (result) {
case Result::Success:
    break;

case Result::Ok:  // 同じcase値になるためエラー
    break;

case Result::Error:
    break;
}

同じ値を別名として定義する場合は、この点に注意が必要です。

enum classでできないこと

列挙子同士をそのまま計算できない

enum class Level {
    Low,
    Medium,
    High
};

auto value = Level::Low + Level::Medium;  // コンパイルエラー

enum classは、数値計算を行うための型ではありません。

どうしても数値として扱う場合は、明示的に変換します。

int value =
    static_cast<int>(Level::Low) +
    static_cast<int>(Level::Medium);

ただし、その計算に本当に意味があるかを確認する必要があります。

++で次の列挙子へ移動できない

Level level = Level::Low;

++level;  // コンパイルエラー

enum classには、標準でインクリメント演算子が定義されていません。

また、列挙子の値が連続しているとは限りません。

enum class Level {
    Low = 1,
    Medium = 10,
    High = 100
};

単純に1を足しても、次の列挙子にはなりません。

enum classが適している場面

選択肢が有限で固定されている

enum class PaymentStatus {
    Pending,
    Paid,
    Failed,
    Refunded
};

状態や選択肢があらかじめ決まっている場合に適しています。

値の意味を明確にしたい

enum class Direction {
    North,
    South,
    East,
    West
};

数値の01で表すよりも、意味が明確です。

型の取り違えを防ぎたい

enum class UserStatus {
    Active,
    Suspended
};

enum class OrderStatus {
    Processing,
    Shipped
};

異なる列挙型は別の型なので、誤って代入したり比較したりできません。

boolでは意味が分かりにくい場合

enum class Visibility {
    Visible,
    Hidden
};

次のようなコードよりも意味が明確になります。

setVisibility(true);
setVisibility(Visibility::Visible);

enum classが適していない場面

値が動的に増える場合

ユーザーが自由に追加するカテゴリや、データベースで管理される項目などには適さない場合があります。

std::string categoryName;

enum classの列挙子は、コンパイル時に固定されるためです。

複数のデータを保持したい場合

商品情報のように、名前、価格、在庫数などを保持したい場合は、構造体やクラスを使用します。

struct Product {
    std::string name;
    int price;
    int stock;
};

状態ごとに複雑なデータや処理が異なる場合

状態ごとに保持するデータや振る舞いが大きく異なる場合は、クラス、継承、std::variantなどのほうが適切なことがあります。

実践的な使用例

注文状態を表す例

#include <iostream>
#include <string_view>

enum class OrderStatus {
    Pending,
    Paid,
    Shipped,
    Delivered,
    Cancelled
};

constexpr std::string_view toString(OrderStatus status)
{
    switch (status) {
    case OrderStatus::Pending:
        return "支払い待ち";

    case OrderStatus::Paid:
        return "支払い済み";

    case OrderStatus::Shipped:
        return "発送済み";

    case OrderStatus::Delivered:
        return "配達完了";

    case OrderStatus::Cancelled:
        return "キャンセル済み";
    }

    return "不明";
}

constexpr bool canCancel(OrderStatus status)
{
    return status == OrderStatus::Pending ||
           status == OrderStatus::Paid;
}

int main()
{
    OrderStatus status = OrderStatus::Paid;

    std::cout << "注文状態: "
              << toString(status)
              << '\n';

    if (canCancel(status)) {
        std::cout << "この注文はキャンセルできます\n";
    }
    else {
        std::cout << "この注文はキャンセルできません\n";
    }
}

このように設計すると、次の利点があります。

  • 不正な文字列を状態として代入しにくい
  • 状態名のタイプミスをコンパイル時に検出できる
  • 使用できる状態の一覧が分かりやすい
  • 関数の引数や戻り値の意味が明確になる
  • 状態追加時の処理漏れを発見しやすい

enum classを使う際の注意点

列挙子以外の値が入る可能性がある

次の列挙型を定義したとします。

enum class Status {
    Success = 0,
    Error = 1
};

Status型の値が、必ずSuccessErrorになるとは限りません。

Status status = static_cast<Status>(50);

そのため、外部入力を変換する場合は値を検証する必要があります。

外部仕様では値を固定する

データベースや通信プロトコルで利用する場合は、列挙子の数値を明示します。

enum class Status : std::uint8_t {
    Success = 0,
    Warning = 1,
    Error = 2
};

ビット演算は必要な型だけに定義する

通常の列挙型は、複数の候補から1つを選ぶために使います。

enum class State {
    Stopped,
    Running,
    Paused
};

ビットフラグは、複数の値を同時に保持するために使います。

enum class Permission {
    Read,
    Write,
    Execute
};

すべての列挙型にビット演算子を定義するのではなく、組み合わせる意味がある型だけに限定することが重要です。

まとめ

enum classは、有限個の名前付き定数を、独立した型として安全に扱うための機能です。

特に重要な特徴は次のとおりです。

  • 列挙子をColor::Redのように型名付きで指定する
  • 整数型やboolへ暗黙変換されない
  • 異なる列挙型を混同しにくい
  • 基底型を指定できる
  • 関数の引数や戻り値の意味を明確にできる
  • switch文による状態分岐と相性がよい

新しくC++コードを書く場合は、通常のenumを使う明確な理由がない限り、enum classを第一候補にするとよいでしょう。

型安全性と可読性が高まり、意図しない変換や値の取り違えを防ぎやすくなります。

以上、C++のenum classについてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次