読み込み中...

C++で定数クラスを効果的に利用する9つの方法

C++で定数クラスを効果的に使うための5つのポイントを表した画像 C++
この記事は約20分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

●定数クラスとは

みなさんは、C++でプログラミングをする際に定数を使ったことはありますか?

定数は変更できない値を表すために使われ、コードの可読性や保守性を高めるのに役立ちます。

しかし、定数をどのように管理するかによって、コードの品質は大きく変わってきます。

そこで登場するのが「定数クラス」です。

定数クラスは、関連する定数をまとめてクラスとして定義する手法です。

定数クラスを使うことで、定数の管理がしやすくなり、コードの質を向上させることができるのです。

○定数クラスの定義と目的

定数クラスは、名前の通り「定数だけを持つクラス」のことを指します。

定数クラスの目的は、関連する定数をグループ化し、一箇所で管理することです。

例えば、ゲームプログラミングでは、キャラクターの状態を表す定数があります。

立っている状態、歩いている状態、走っている状態など、様々な状態があり、それぞれに対応する定数が必要です。

これらの定数を個別に定義していては管理が大変ですし、コードの可読性も下がってしまいます。

そこで、キャラクターの状態に関する定数を一つのクラスにまとめて定義するのです。

こうすることで、定数の管理がしやすくなり、コードの可読性も高まります。

○定数クラスを使うメリット

定数クラスを使うメリットは大きく分けて3つあります。

1つ目は、定数の管理がしやすくなることです。

関連する定数を一箇所にまとめることで、定数の追加や変更が容易になります。

また、定数の重複を防ぐこともできます。

2つ目は、コードの可読性が向上することです。

定数クラスを使えば、定数の意味がわかりやすくなります。

例えば、「キャラクターの状態」という定数クラスがあれば、その中の定数は全てキャラクターの状態を表していることが一目瞭然です。

3つ目は、コードの保守性が高まることです。

定数クラスを使えば、定数の変更が他のコードに与える影響を最小限に抑えられます。

定数の値を変更しても、定数クラスを使っている箇所では、自動的に新しい値が反映されるからです。

○定数クラスの命名規則

定数クラスを作る際は、命名規則に気をつける必要があります。

定数クラスは、他のクラスと区別しやすいように、名前の末尾に「Constants」をつけるのが一般的です。

例えば、キャラクターの状態を表す定数クラスは「CharacterStateConstants」、ゲームの設定を表す定数クラスは「GameSettingConstants」というような具合です。

また、定数クラスの中で定義する定数の名前は、すべて大文字で単語をアンダースコアで区切るのが通例です。

例えば、「IDLE_STATE」「WALKING_STATE」「RUNNING_STATE」といった具合です。

このような命名規則を守ることで、定数クラスとそれ以外のクラスを明確に区別でき、コードの可読性が高まります。

●定数クラスの作成方法

定数クラスを使うメリットがわかったところで、次は実際の作成方法について見ていきましょう。

定数クラスの実装には、いくつかの方法があります。

それぞれの特徴を理解して、状況に応じて適切な方法を選ぶことが大切ですよ。

○サンプルコード1:基本的な定数クラスの実装

まずは、最も基本的な定数クラスの実装方法から見ていきましょう。

下記のサンプルコードをご覧ください。

class Constants {
public:
    static const int MIN_VALUE = 0;
    static const int MAX_VALUE = 100;
    static const std::string DEFAULT_NAME;
};

const std::string Constants::DEFAULT_NAME = "名無し";

このコードでは、Constantsクラスの中でMIN_VALUEMAX_VALUEDEFAULT_NAMEの3つの定数を定義しています。

MIN_VALUEMAX_VALUEはint型の定数、DEFAULT_NAMEはstring型の定数です。

DEFAULT_NAMEのように、クラス内で初期値を与えられない定数は、クラス定義の外で初期化する必要があります。

ここでは、const std::string Constants::DEFAULT_NAME = "名無し";という形で初期化しています。

このような形で定数を定義しておけば、他のコードからConstants::MIN_VALUEのようにアクセスできます。

定数の値を変更する必要が出てきたら、Constantsクラスのコードを修正するだけで済むので、メンテナンス性が高いコードになるのです。

○サンプルコード2:名前空間を使った定数クラスの実装

次に、名前空間を使った定数クラスの実装方法を見ていきましょう。

名前空間を使うと、定数の名前が衝突するのを防げます。

下記のサンプルコードをご覧ください。

namespace GameConstants {
    const int MIN_SCORE = 0;
    const int MAX_SCORE = 1000;
    const std::string GAME_TITLE = "マイゲーム";
}

このコードでは、GameConstantsという名前空間の中で定数を定義しています。

名前空間を使えば、他の名前空間や他のコードと定数名が衝突する心配がありません。

名前空間を使った定数クラスにアクセスするには、GameConstants::MIN_SCOREのように、名前空間名とスコープ解決演算子::を使います。

○サンプルコード3:enumを使った定数クラスの実装

3つ目は、enumを使った定数クラスの実装方法です。

enumを使うと、定数に連番の値を自動で割り当ててくれます。

下記のサンプルコードをご覧ください。

class CharacterState {
public:
    enum State {
        IDLE,
        WALKING,
        RUNNING,
        JUMPING,
    };
};

このコードでは、CharacterStateクラスの中でenumを使って定数を定義しています。

Stateというenumの中で、IDLEWALKINGRUNNINGJUMPINGの4つの定数を定義しています。

enumを使うと、IDLEには0、WALKINGには1、RUNNINGには2、JUMPINGには3という値が自動で割り当てられます。

これらの定数にアクセスするには、CharacterState::IDLEのように、クラス名とスコープ解決演算子::を使います。

○サンプルコード4:constexprを使った定数クラスの実装

最後は、C++11で導入されたconstexprを使った定数クラスの実装方法です。

constexprを使うと、コンパイル時に定数の値が決定されるので、パフォーマンスが向上します。

下記のサンプルコードをご覧ください。

class MathConstants {
public:
    static constexpr double PI = 3.14159265358979323846;
    static constexpr double E  = 2.71828182845904523536;
};

このコードでは、MathConstantsクラスの中でconstexprを使って数学定数を定義しています。

PIEの2つの定数を、constexprを使って定義しています。

constexprを使った定数は、コンパイル時に値が決まるため、実行時のオーバーヘッドがありません。

また、ヘッダファイルの中で完結して定義できるので、コードの見通しが良くなります。

constexprを使った定数にアクセスするには、MathConstants::PIのように、クラス名とスコープ解決演算子::を使います。

●定数クラス作成時の注意点

定数クラスを作成する際は、いくつか注意しなければならない点があります。

定数クラスを適切に使用するために、これらの注意点を理解しておくことが大切ですよ。

○スコープを限定する

定数クラスを作成する際は、スコープを限定することを心がけましょう。

グローバルスコープに定数を定義すると、名前の衝突が起きやすくなります。

定数クラスを使う目的の1つが、定数の管理をしやすくすることです。

そのためにも、定数のスコープはなるべく限定したいものです。

具体的には、定数クラスは名前空間の中で定義するのが良いでしょう。

また、クラスの中で定数を定義する場合は、そのクラス内でのみアクセスできるようにします。

namespace GameSettings {
    class Constants {
    public:
        static const int WINDOW_WIDTH = 800;
        static const int WINDOW_HEIGHT = 600;
    };
}

このコードでは、ConstantsクラスをGameSettings名前空間の中で定義しています。

こうすることで、Constantsクラスの定数のスコープを限定できます。

○静的メンバ変数の初期化に注意

定数クラスで静的メンバ変数を定義する場合は、初期化の方法に注意が必要です。

静的メンバ変数は、クラス定義の中では宣言だけを行い、定義は.cppファイルの中で行います。

この際、const修飾子をつけて初期化を行う必要があります。

// Constants.h
class Constants {
public:
    static const std::string DEFAULT_NAME;
};

// Constants.cpp
const std::string Constants::DEFAULT_NAME = "名無しさん";

このように、ヘッダファイルでは静的メンバ変数の宣言だけを行い、.cppファイルの中でconst修飾子をつけて初期化します。

初期化を忘れると、リンクエラーが発生してしまうので注意しましょう。

○巨大な定数クラスを避ける

定数クラスは便利な反面、巨大になりすぎると逆に管理が難しくなってしまいます。

定数クラスが巨大になると、必要な定数を見つけるのが大変になります。

また、コンパイル時間が長くなったり、メモリ使用量が増えたりする可能性もあります。

そのため、定数クラスは関連する定数をグループ化し、適度な大きさに分割するのが良いでしょう。

例えば、ゲームの設定に関する定数を1つの定数クラスにまとめるのではなく、ウィンドウ設定、サウンド設定、ゲームバランス設定などに分割するのです。

namespace GameSettings {
    class WindowConstants {
    public:
        static const int WINDOW_WIDTH = 800;
        static const int WINDOW_HEIGHT = 600;
    };

    class SoundConstants {
    public:
        static const int MASTER_VOLUME = 80;
        static const int BGM_VOLUME = 70;
    };

    class GameBalanceConstants {
    public:
        static const int PLAYER_MAX_HP = 100;
        static const int ENEMY_MAX_HP = 80;
    };
}

このように、関連する定数をグループ化し、複数の定数クラスに分割することで、定数の管理がしやすくなります。

○可読性を重視したコーディングスタイル

定数クラスを作成する際は、可読性を重視したコーディングスタイルを心がけましょう。

定数名は、すべて大文字で単語をアンダースコアで区切るのが一般的です。

また、定数名は定数の意味がわかりやすいものにします。

class Constants {
public:
    static const int MAX_PLAYER_COUNT = 4;
    static const std::string CONFIG_FILE_PATH = "config.ini";
};

このように、定数名はMAX_PLAYER_COUNTCONFIG_FILE_PATHのように、定数の意味がわかりやすいものにしましょう。

また、定数クラスの中では、publicセクションに定数を定義し、privateセクションにはできるだけ何も書かないようにします。

こうすることで、定数クラスの可読性が高まります。

●定数クラスのアンチパターン

定数クラスは便利な機能ですが、使い方を間違えるとかえってコードの可読性や保守性を下げてしまうことがあります。

そのような定数クラスのアンチパターンについて、具体例を交えて見ていきましょう。

○サンプルコード5:アンチパターンの例とその理由

まずは、定数クラスのアンチパターンの例を見てみましょう。

class Constants {
public:
    static const int WINDOW_WIDTH = 800;
    static const int WINDOW_HEIGHT = 600;
    static const int PLAYER_MAX_HP = 100;
    static const int ENEMY_MAX_HP = 80;
    static const std::string CONFIG_FILE_PATH = "config.ini";
    static const std::string SAVE_DATA_PATH = "save/";
    static const std::string TEXTURE_PATH = "textures/";
    static const std::string SOUND_PATH = "sounds/";
};

このコードでは、ゲームプログラムで使用する様々な定数を1つの定数クラスにまとめています。

ウィンドウのサイズ、プレイヤーや敵のHP、各種ファイルパスなど、関連性の低い定数が混在しています。

こんな風に関連性の低い定数をまとめてしまうと、定数クラスが巨大になり、可読性が下がってしまいます。

また、定数の追加や変更の際に、他の定数に影響を与えてしまう可能性もあります。

定数クラスを使う目的は、定数の管理をしやすくすることです。

そのためには、関連する定数だけをグループ化し、適度な大きさの定数クラスに分割することが大切なのです。

○サンプルコード6:アンチパターンを改善した例

先ほどのアンチパターンの例を改善してみましょう。

namespace GameSettings {
    class WindowConstants {
    public:
        static const int WIDTH = 800;
        static const int HEIGHT = 600;
    };

    class CharacterConstants {
    public:
        static const int PLAYER_MAX_HP = 100;
        static const int ENEMY_MAX_HP = 80;
    };

    class PathConstants {
    public:
        static const std::string CONFIG_FILE = "config.ini";
        static const std::string SAVE_DATA_DIR = "save/";
        static const std::string TEXTURE_DIR = "textures/";
        static const std::string SOUND_DIR = "sounds/";
    };
}

このコードでは、関連する定数だけをグループ化し、3つの定数クラスに分割しています。

ウィンドウ関連の定数はWindowConstantsクラス、キャラクター関連の定数はCharacterConstantsクラス、ファイルパス関連の定数はPathConstantsクラスにまとめました。

また、定数名から_PATHなどの_を除き、WIDTHHEIGHTのようにシンプルな名前にしています。

ファイルパス関連の定数名は、_FILE_DIRのように、ファイルなのかディレクトリなのかを末尾に付けました。

このように定数クラスを分割することで、定数の管理がしやすくなり、コードの可読性も高まります。

○文字列定数の扱い方

定数クラスで文字列定数を扱う場合は、特に注意が必要です。

文字列定数を大量に定数クラスに定義すると、クラスが巨大になり、メンテナンス性が下がります。

また、文字列定数の値を変更する際に、プログラムの再コンパイルが必要になるのも問題です。

そのため、文字列定数はなるべく定数クラスに定義するのは避け、外部ファイルから読み込むようにするのが良いでしょう。

外部ファイルから読み込めば、プログラムの再コンパイルをせずに文字列定数を変更できます。

class StringConstants {
public:
    static const std::string CONFIG_FILE_PATH;
    static const std::string DATA_FILE_PATH;
};

const std::string StringConstants::CONFIG_FILE_PATH = "config.ini";
const std::string StringConstants::DATA_FILE_PATH = "data.json";

このコードでは、CONFIG_FILE_PATHDATA_FILE_PATHという2つの文字列定数を定数クラスに定義しています。

しかし、文字列定数の値はプログラムコード内で直接指定するのではなく、config.inidata.jsonといった外部ファイルから読み込むようにすると良いでしょう。

// 実行結果
// config.iniファイルの内容を読み込んで処理を行う

このように、文字列定数は外部ファイルから読み込むことで、プログラムの再コンパイルなしに定数の値を変更できます。

●他言語との違いと応用例

C++の定数クラスについて理解が深まったところで、今度は他言語での定数の扱い方と比較してみましょう。

他言語との違いを知ることで、C++の定数クラスの特徴がより明確になります。

○VBAやJavaとの定数クラスの違い

VBAやJavaでは、C++の定数クラスに相当する機能があるでしょうか?

VBAには、定数を定義するためのConstキーワードがあります。

Constキーワードを使って、変数に定数値を割り当てることができます。

Const PI As Double = 3.14159265358979

このように、VBAでは定数クラスを使わずに定数を定義できます。

ただし、VBAの定数は関数やプロシージャ内でのみ使用できるので、グローバルに定数を定義することはできません。

一方、Javaには、finalキーワードを使った定数の定義方法があります。

finalキーワードを使えば、クラスのフィールドを定数として定義できます。

public class MathConstants {
    public static final double PI = 3.14159265358979;
    public static final double E = 2.71828182845904;
}

このように、JavaではC++の定数クラスと同様の方法で定数を定義できます。

ただし、Javaの定数はすべてstatic finalにする必要があり、コンパイル時に値が決定される必要があります。

C++の定数クラスは、constexprキーワードを使えば、コンパイル時に値が決定される定数を定義できます。

また、const staticデータメンバーを使えば、実行時に初期化される定数も定義できます。この点が、VBAやJavaとの大きな違いと言えるでしょう。

○サンプルコード7:クラス固有の定数の定義

次に、クラス固有の定数の定義方法について見ていきましょう。

クラス固有の定数とは、特定のクラスの中でのみ使用される定数のことです。

例えば、あるクラスの中で頻繁に使用される定数値があるとします。

そのような定数は、クラスの中で定義するのが良いでしょう。

class MyClass {
private:
    static const int MAX_COUNT = 100;
    static const double THRESHOLD = 0.5;

public:
    void doSomething() {
        for (int i = 0; i < MAX_COUNT; ++i) {
            // 処理の内容
        }
        if (someValue > THRESHOLD) {
            // 処理の内容
        }
    }
};

このコードでは、MyClassの中でMAX_COUNTTHRESHOLDという2つの定数を定義しています。

これらの定数はMyClassの中でのみ使用されるので、クラスの中で定義するのが適切です。

クラス固有の定数を定義することで、定数の意味がわかりやすくなり、コードの可読性が高まります。

○サンプルコード8:const修飾子を使った定数の宣言

C++では、const修飾子を使って定数を宣言することもできます。

const int MAX_VALUE = 1000;
const double PI = 3.14159265358979;

class MyClass {
public:
    MyClass(const std::string& name)
        : m_name(name) {}

    const std::string& getName() const {
        return m_name;
    }

private:
    const std::string m_name;
};

このコードでは、まずMAX_VALUEPIという2つの定数をグローバルスコープで定義しています。

これらの定数は、プログラム全体で使用できます。

次に、MyClassの中でm_nameというconst修飾子付きのメンバ変数を定義しています。

const修飾子を付けることで、m_nameは一度初期化されたら変更できなくなります。

また、getName関数にもconst修飾子を付けています。

これは、getName関数がオブジェクトの状態を変更しないことを表しています。

const修飾子を使うことで、変数や関数の意図を明確にできます。

これにより、コードの可読性と保守性が高まるのです。

○サンプルコード9:enumを使った定数の宣言

最後に、enumを使った定数の宣言方法について見ていきましょう。

enumを使うと、関連する定数をグループ化して宣言できます。

enum class Color {
    Red,
    Green,
    Blue,
};

void setColor(Color color) {
    switch (color) {
        case Color::Red:
            // 赤色の処理
            break;
        case Color::Green:
            // 緑色の処理
            break;
        case Color::Blue:
            // 青色の処理
            break;
    }
}

// 実行結果
// setColor関数に Color::Red を渡すと、赤色の処理が実行される

このコードでは、Colorというenum classを定義し、RedGreenBlueという3つの定数を宣言しています。

setColor関数はColor型の引数を取ります。

switch文を使って、渡されたColorの値に応じて処理を振り分けています。

enumを使うことで、関連する定数をグループ化でき、コードの可読性が高まります。

また、enum classを使えば、名前空間の汚染を防ぐことができます。

まとめ

C++の定数クラスについて、基本的な使い方から応用例まで詳しく解説してきました。

定数クラスを作成する際は、スコープを限定したり、静的メンバ変数の初期化に気をつけたりと、いくつかの注意点があります。

また、アンチパターンを避け、可読性の高いコーディングスタイルを心がけることも大切です。

他言語との比較を通して、C++の定数クラスの特徴も理解できたのではないでしょうか。

クラス固有の定数やenum、const修飾子などを適切に使い分けることで、より良いコードを書くことができます。