●定数クラスとは
みなさんは、C++でプログラミングをする際に定数を使ったことはありますか?
定数は変更できない値を表すために使われ、コードの可読性や保守性を高めるのに役立ちます。
しかし、定数をどのように管理するかによって、コードの品質は大きく変わってきます。
そこで登場するのが「定数クラス」です。
定数クラスは、関連する定数をまとめてクラスとして定義する手法です。
定数クラスを使うことで、定数の管理がしやすくなり、コードの質を向上させることができるのです。
○定数クラスの定義と目的
定数クラスは、名前の通り「定数だけを持つクラス」のことを指します。
定数クラスの目的は、関連する定数をグループ化し、一箇所で管理することです。
例えば、ゲームプログラミングでは、キャラクターの状態を表す定数があります。
立っている状態、歩いている状態、走っている状態など、様々な状態があり、それぞれに対応する定数が必要です。
これらの定数を個別に定義していては管理が大変ですし、コードの可読性も下がってしまいます。
そこで、キャラクターの状態に関する定数を一つのクラスにまとめて定義するのです。
こうすることで、定数の管理がしやすくなり、コードの可読性も高まります。
○定数クラスを使うメリット
定数クラスを使うメリットは大きく分けて3つあります。
1つ目は、定数の管理がしやすくなることです。
関連する定数を一箇所にまとめることで、定数の追加や変更が容易になります。
また、定数の重複を防ぐこともできます。
2つ目は、コードの可読性が向上することです。
定数クラスを使えば、定数の意味がわかりやすくなります。
例えば、「キャラクターの状態」という定数クラスがあれば、その中の定数は全てキャラクターの状態を表していることが一目瞭然です。
3つ目は、コードの保守性が高まることです。
定数クラスを使えば、定数の変更が他のコードに与える影響を最小限に抑えられます。
定数の値を変更しても、定数クラスを使っている箇所では、自動的に新しい値が反映されるからです。
○定数クラスの命名規則
定数クラスを作る際は、命名規則に気をつける必要があります。
定数クラスは、他のクラスと区別しやすいように、名前の末尾に「Constants」をつけるのが一般的です。
例えば、キャラクターの状態を表す定数クラスは「CharacterStateConstants」、ゲームの設定を表す定数クラスは「GameSettingConstants」というような具合です。
また、定数クラスの中で定義する定数の名前は、すべて大文字で単語をアンダースコアで区切るのが通例です。
例えば、「IDLE_STATE」「WALKING_STATE」「RUNNING_STATE」といった具合です。
このような命名規則を守ることで、定数クラスとそれ以外のクラスを明確に区別でき、コードの可読性が高まります。
●定数クラスの作成方法
定数クラスを使うメリットがわかったところで、次は実際の作成方法について見ていきましょう。
定数クラスの実装には、いくつかの方法があります。
それぞれの特徴を理解して、状況に応じて適切な方法を選ぶことが大切ですよ。
○サンプルコード1:基本的な定数クラスの実装
まずは、最も基本的な定数クラスの実装方法から見ていきましょう。
下記のサンプルコードをご覧ください。
このコードでは、Constants
クラスの中でMIN_VALUE
、MAX_VALUE
、DEFAULT_NAME
の3つの定数を定義しています。
MIN_VALUE
とMAX_VALUE
はint型の定数、DEFAULT_NAME
はstring型の定数です。
DEFAULT_NAME
のように、クラス内で初期値を与えられない定数は、クラス定義の外で初期化する必要があります。
ここでは、const std::string Constants::DEFAULT_NAME = "名無し";
という形で初期化しています。
このような形で定数を定義しておけば、他のコードからConstants::MIN_VALUE
のようにアクセスできます。
定数の値を変更する必要が出てきたら、Constants
クラスのコードを修正するだけで済むので、メンテナンス性が高いコードになるのです。
○サンプルコード2:名前空間を使った定数クラスの実装
次に、名前空間を使った定数クラスの実装方法を見ていきましょう。
名前空間を使うと、定数の名前が衝突するのを防げます。
下記のサンプルコードをご覧ください。
このコードでは、GameConstants
という名前空間の中で定数を定義しています。
名前空間を使えば、他の名前空間や他のコードと定数名が衝突する心配がありません。
名前空間を使った定数クラスにアクセスするには、GameConstants::MIN_SCORE
のように、名前空間名とスコープ解決演算子::
を使います。
○サンプルコード3:enumを使った定数クラスの実装
3つ目は、enumを使った定数クラスの実装方法です。
enumを使うと、定数に連番の値を自動で割り当ててくれます。
下記のサンプルコードをご覧ください。
このコードでは、CharacterState
クラスの中でenumを使って定数を定義しています。
State
というenumの中で、IDLE
、WALKING
、RUNNING
、JUMPING
の4つの定数を定義しています。
enumを使うと、IDLE
には0、WALKING
には1、RUNNING
には2、JUMPING
には3という値が自動で割り当てられます。
これらの定数にアクセスするには、CharacterState::IDLE
のように、クラス名とスコープ解決演算子::
を使います。
○サンプルコード4:constexprを使った定数クラスの実装
最後は、C++11で導入されたconstexprを使った定数クラスの実装方法です。
constexprを使うと、コンパイル時に定数の値が決定されるので、パフォーマンスが向上します。
下記のサンプルコードをご覧ください。
このコードでは、MathConstants
クラスの中でconstexprを使って数学定数を定義しています。
PI
とE
の2つの定数を、constexprを使って定義しています。
constexprを使った定数は、コンパイル時に値が決まるため、実行時のオーバーヘッドがありません。
また、ヘッダファイルの中で完結して定義できるので、コードの見通しが良くなります。
constexprを使った定数にアクセスするには、MathConstants::PI
のように、クラス名とスコープ解決演算子::
を使います。
●定数クラス作成時の注意点
定数クラスを作成する際は、いくつか注意しなければならない点があります。
定数クラスを適切に使用するために、これらの注意点を理解しておくことが大切ですよ。
○スコープを限定する
定数クラスを作成する際は、スコープを限定することを心がけましょう。
グローバルスコープに定数を定義すると、名前の衝突が起きやすくなります。
定数クラスを使う目的の1つが、定数の管理をしやすくすることです。
そのためにも、定数のスコープはなるべく限定したいものです。
具体的には、定数クラスは名前空間の中で定義するのが良いでしょう。
また、クラスの中で定数を定義する場合は、そのクラス内でのみアクセスできるようにします。
このコードでは、Constants
クラスをGameSettings
名前空間の中で定義しています。
こうすることで、Constants
クラスの定数のスコープを限定できます。
○静的メンバ変数の初期化に注意
定数クラスで静的メンバ変数を定義する場合は、初期化の方法に注意が必要です。
静的メンバ変数は、クラス定義の中では宣言だけを行い、定義は.cppファイルの中で行います。
この際、const修飾子をつけて初期化を行う必要があります。
このように、ヘッダファイルでは静的メンバ変数の宣言だけを行い、.cppファイルの中でconst修飾子をつけて初期化します。
初期化を忘れると、リンクエラーが発生してしまうので注意しましょう。
○巨大な定数クラスを避ける
定数クラスは便利な反面、巨大になりすぎると逆に管理が難しくなってしまいます。
定数クラスが巨大になると、必要な定数を見つけるのが大変になります。
また、コンパイル時間が長くなったり、メモリ使用量が増えたりする可能性もあります。
そのため、定数クラスは関連する定数をグループ化し、適度な大きさに分割するのが良いでしょう。
例えば、ゲームの設定に関する定数を1つの定数クラスにまとめるのではなく、ウィンドウ設定、サウンド設定、ゲームバランス設定などに分割するのです。
このように、関連する定数をグループ化し、複数の定数クラスに分割することで、定数の管理がしやすくなります。
○可読性を重視したコーディングスタイル
定数クラスを作成する際は、可読性を重視したコーディングスタイルを心がけましょう。
定数名は、すべて大文字で単語をアンダースコアで区切るのが一般的です。
また、定数名は定数の意味がわかりやすいものにします。
このように、定数名はMAX_PLAYER_COUNT
やCONFIG_FILE_PATH
のように、定数の意味がわかりやすいものにしましょう。
また、定数クラスの中では、publicセクションに定数を定義し、privateセクションにはできるだけ何も書かないようにします。
こうすることで、定数クラスの可読性が高まります。
●定数クラスのアンチパターン
定数クラスは便利な機能ですが、使い方を間違えるとかえってコードの可読性や保守性を下げてしまうことがあります。
そのような定数クラスのアンチパターンについて、具体例を交えて見ていきましょう。
○サンプルコード5:アンチパターンの例とその理由
まずは、定数クラスのアンチパターンの例を見てみましょう。
このコードでは、ゲームプログラムで使用する様々な定数を1つの定数クラスにまとめています。
ウィンドウのサイズ、プレイヤーや敵のHP、各種ファイルパスなど、関連性の低い定数が混在しています。
こんな風に関連性の低い定数をまとめてしまうと、定数クラスが巨大になり、可読性が下がってしまいます。
また、定数の追加や変更の際に、他の定数に影響を与えてしまう可能性もあります。
定数クラスを使う目的は、定数の管理をしやすくすることです。
そのためには、関連する定数だけをグループ化し、適度な大きさの定数クラスに分割することが大切なのです。
○サンプルコード6:アンチパターンを改善した例
先ほどのアンチパターンの例を改善してみましょう。
このコードでは、関連する定数だけをグループ化し、3つの定数クラスに分割しています。
ウィンドウ関連の定数はWindowConstants
クラス、キャラクター関連の定数はCharacterConstants
クラス、ファイルパス関連の定数はPathConstants
クラスにまとめました。
また、定数名から_PATH
などの_
を除き、WIDTH
やHEIGHT
のようにシンプルな名前にしています。
ファイルパス関連の定数名は、_FILE
や_DIR
のように、ファイルなのかディレクトリなのかを末尾に付けました。
このように定数クラスを分割することで、定数の管理がしやすくなり、コードの可読性も高まります。
○文字列定数の扱い方
定数クラスで文字列定数を扱う場合は、特に注意が必要です。
文字列定数を大量に定数クラスに定義すると、クラスが巨大になり、メンテナンス性が下がります。
また、文字列定数の値を変更する際に、プログラムの再コンパイルが必要になるのも問題です。
そのため、文字列定数はなるべく定数クラスに定義するのは避け、外部ファイルから読み込むようにするのが良いでしょう。
外部ファイルから読み込めば、プログラムの再コンパイルをせずに文字列定数を変更できます。
このコードでは、CONFIG_FILE_PATH
とDATA_FILE_PATH
という2つの文字列定数を定数クラスに定義しています。
しかし、文字列定数の値はプログラムコード内で直接指定するのではなく、config.ini
やdata.json
といった外部ファイルから読み込むようにすると良いでしょう。
このように、文字列定数は外部ファイルから読み込むことで、プログラムの再コンパイルなしに定数の値を変更できます。
●他言語との違いと応用例
C++の定数クラスについて理解が深まったところで、今度は他言語での定数の扱い方と比較してみましょう。
他言語との違いを知ることで、C++の定数クラスの特徴がより明確になります。
○VBAやJavaとの定数クラスの違い
VBAやJavaでは、C++の定数クラスに相当する機能があるでしょうか?
VBAには、定数を定義するためのConstキーワードがあります。
Constキーワードを使って、変数に定数値を割り当てることができます。
このように、VBAでは定数クラスを使わずに定数を定義できます。
ただし、VBAの定数は関数やプロシージャ内でのみ使用できるので、グローバルに定数を定義することはできません。
一方、Javaには、finalキーワードを使った定数の定義方法があります。
finalキーワードを使えば、クラスのフィールドを定数として定義できます。
このように、JavaではC++の定数クラスと同様の方法で定数を定義できます。
ただし、Javaの定数はすべてstatic finalにする必要があり、コンパイル時に値が決定される必要があります。
C++の定数クラスは、constexprキーワードを使えば、コンパイル時に値が決定される定数を定義できます。
また、const staticデータメンバーを使えば、実行時に初期化される定数も定義できます。この点が、VBAやJavaとの大きな違いと言えるでしょう。
○サンプルコード7:クラス固有の定数の定義
次に、クラス固有の定数の定義方法について見ていきましょう。
クラス固有の定数とは、特定のクラスの中でのみ使用される定数のことです。
例えば、あるクラスの中で頻繁に使用される定数値があるとします。
そのような定数は、クラスの中で定義するのが良いでしょう。
このコードでは、MyClass
の中でMAX_COUNT
とTHRESHOLD
という2つの定数を定義しています。
これらの定数はMyClass
の中でのみ使用されるので、クラスの中で定義するのが適切です。
クラス固有の定数を定義することで、定数の意味がわかりやすくなり、コードの可読性が高まります。
○サンプルコード8:const修飾子を使った定数の宣言
C++では、const修飾子を使って定数を宣言することもできます。
このコードでは、まずMAX_VALUE
とPI
という2つの定数をグローバルスコープで定義しています。
これらの定数は、プログラム全体で使用できます。
次に、MyClass
の中でm_name
というconst修飾子付きのメンバ変数を定義しています。
const修飾子を付けることで、m_name
は一度初期化されたら変更できなくなります。
また、getName
関数にもconst修飾子を付けています。
これは、getName
関数がオブジェクトの状態を変更しないことを表しています。
const修飾子を使うことで、変数や関数の意図を明確にできます。
これにより、コードの可読性と保守性が高まるのです。
○サンプルコード9:enumを使った定数の宣言
最後に、enumを使った定数の宣言方法について見ていきましょう。
enumを使うと、関連する定数をグループ化して宣言できます。
このコードでは、Color
というenum classを定義し、Red
、Green
、Blue
という3つの定数を宣言しています。
setColor
関数はColor
型の引数を取ります。
switch
文を使って、渡されたColor
の値に応じて処理を振り分けています。
enumを使うことで、関連する定数をグループ化でき、コードの可読性が高まります。
また、enum classを使えば、名前空間の汚染を防ぐことができます。
まとめ
C++の定数クラスについて、基本的な使い方から応用例まで詳しく解説してきました。
定数クラスを作成する際は、スコープを限定したり、静的メンバ変数の初期化に気をつけたりと、いくつかの注意点があります。
また、アンチパターンを避け、可読性の高いコーディングスタイルを心がけることも大切です。
他言語との比較を通して、C++の定数クラスの特徴も理解できたのではないでしょうか。
クラス固有の定数やenum、const修飾子などを適切に使い分けることで、より良いコードを書くことができます。