【学生さん向け】読みやすいC++コードを書くためのポイント

プログラマーの尾関です。

先日、学生さんの作品を添削する機会があり、作品はどれもが若さに満ち溢れていて、荒々しいながらもフレッシュな作品が数多くあり、昔を思い出しながらも新鮮な気持ちにさせていただきました。

その場での話をもとに、この記事では、読みやすいC++コードを書くためのポイントを紹介します。

コメントの書き方

ソースコードから処理が自明な行には (なるべく) コメントを入れない

これは、明らかに意味が取れるようなコードにはコメントは不要であることが多い、という理由があります。

int hogeValue = 0; // パラメータを0で初期化する

例えば上記コードは、コードで行っていることを「そのまま」説明しているだけです。こういったコメントを数多く書いてしまうと、コード内の情報量が増えてしまい、可読性が低下します。そしてもしコード内の処理が変わった時に「コードの修正」+「コメントの修正」という2重の変更が必要となってしまい、コードの保守という点でもあまり良くありません。

そういうこともあって、自明な部分にはなるべくコメントを入れない方が良い、となります。

では、必ずしもこういった自明なコメントが「絶対に不要」なのか…というと、限らずしもそうとは言えません。例えば以下のような場合には「処理1」と「処理2」の区切りとして行う初期化なのだな…、ということが「後からわかる」コメントとなります。

// 何かの処理1
// ...

hogeValue = 0; // 次の処理のために初期化を挟みます.

// 何かの処理2
// ...

この添削会でも繰り返し話したこととして、「コードを書いて、それを例えば3日後に見直したときに理解できるのか?」といった考え方がありました。もし3日後に見直したときに理解に時間がかからないように、事前にわかりやすく書いておく、というのが1つの答えとなります。

関数の戻り値・引数の仕様をヘッダファイルのコメントに記載する

関数は内部で行う処理だけでなく、返却する戻り値の条件、引数として有効なパラメータなど多くの条件が求められます。そのため関数にはしっかりとコメントを書いておくことが大切です。

class hogeClass
{
public:
    static const int MAX_VALUE = 100;


    // 何らかの処理をする関数
    // 戻り値: エラーが発生しなかった場合0、objがnullの場合-1、value > MAX_VALUEの場合-2をエラー値として返す
    // obj: 処理を施すObj型のオブジェクトnullptrを渡した場合エラーになる
    // value: 何らかの処理に使う値,MAX_VALUE以下の値を入力する必要がある
    int Function(const Obj* obj, int value);
}

またポインタを引数とする場合は、nullptr を受け付けるかどうか、数値の場合には有効な値の範囲を記載しておくとなお良いと思います。

コーディングについて

マジックナンバーは使わずに「わかりやすい名前」の「定数」を使う

プログラムを動かすためにとりあえず動作する値(下記の例であれば "100" ) を記述してしまうことはよくあると思います。ですが、そういった「マジックナンバー」はあとから見たときに何を意味するのかがわからなくなってしまいます。

そこで「static const」などで定数として記述し、それを使うようにすることで、定数の名前から数値の意味がわかるようになります。

// not good
int value = 100;

// good
static const int MAX_VALUE = 100; // 最大値
int value = MAX_VALUE;

{}のブロックの表記ゆれに注意する

例えば{}を使ったブロックの書き方は色々な流派があります。どれが正しいのかはプロジェクトやコードする人次第ですが、混在してはいけません。

if(...){
    func();
}

// not good (場所によって書き方が異なる)
if(...)
{
    func2();
}

例えば ifと同じ行に "{" を入れると決めたら、その書き方で可能な限り統一します。

変数宣言時に初期化する

C/C++言語は言語仕様上、変数の宣言時に自動で初期化を行いません。そのため変数宣言時に初期化を行うようにすることで「初期化忘れ」を防ぐことができ、コードとしても初期化部分がわかりやすくなるメリットがあります。

class hoge
{
public:
    // not good
    int value = 0;

    // good
    int m_value = 0;
};

出力引数には「out」の接頭語をつける

引数をポインタ渡し、または参照渡しにして戻り値としても使う場合には、変数名に「out」の接頭語をつけることで、宣言部分から出力引数であることが明示的となります。

// プレイヤーパラメータの取得。取得失敗時にはfalseを返す.
bool GetParameter(PlayerParameter* pOutParameter) {
  ...
}

Get関数は定数関数にする (constをつける)

Get関数は「値の取得のみ」を行うはずなので、constをつけて定数関数にします。

class Hoge {
public:
    int GetValue() const { return m_value; };

private:
    int m_value = 0;
};
Information

もし副作用がある場合には、そもそもGet関数ではないので、設計を見直したり別の名前にする必要があります。

叙述関数を使用する

状態の判定などboolを返す叙述関数は、わかりやすい名前をつけるのが良いです。叙述関数には以下の例があります。

  • Is~   :状態が~である
  • Has~:~を保持している
  • Can~:~をすることができる
class Character {
public:
    // 生きているかどうか.
    bool IsAlive() const { return m_isAlive; }
    // 指定のアイテムを所持しているかどうか.
    bool HasItem(int itemId) const;
    // ターン開始時に行動可能かどうか.
    bool CanActionableAtTurnBeginning() const;

private:
    bool m_active;
};
Information

叙述関数はboolを返すGet関数とも言えるため、定数関数 (constをつける) を使うようにします

定数には#defineよりもstatic constを使う

#defineはデータ型の束縛がなく、単なるコード置き換えであるため意図しない置き換えが発生する可能性があります。

そのため、定数を使いたい場合には「static const」キーワードを使って型の安全性を確保する方が、わかりやすく安全なコードとなります。

// not good
#define CONST_VALUE 0

// good
static const int CONST_VALUE = 0;

状態はenum(列挙型)を使うとわかりやすくなる

例えば「方向」など状態を持つ変数はenumを使うとわかりやすいコードとなります。

// not good
int direction = 0;

// good
enum class DIRECTION
{
    FRONT,
    BACK,
    LEFT,
    RIGHT,
};

DIRECTION direction = DIRECTION::FRONT;

メモリの扱い

deleteマクロを使う

メモリを破棄する場合のキーワード「delete」は直接使うのではなく、専用のマクロ SAFE_DELETE を作ることをおすすめします 。

// pがnullptr の場合のみdeleteします。またポインタにnullptrを入れます
#define SAFE_DELETE(p) { if(p) { delete (p); (p)=nullptr; } }
Information

このマクロを使うことで、メモリの2重解放や解放済みポインタにnullptr入れ忘れることを回避することができます。

newは動的確保が絶対に必要な場面でのみ使う

C++はnewでメモリを動的確保すると、deleteでメモリを破棄する必要があります。delete書き忘れは注意していても発生しやすい問題ですので、newを書かなくて良いなら書かない記述方法がおすすめです。

例えばメンバ変数をポインタで宣言すると…

class Hoge {
private:
  Object* m_pObject = nullptr;
};

newとdeleteをセットで呼び出す問題が発生します。

void Hoge::Create() {
  m_pObject = new Object(); // 生成
}
...
void Hoge::Delete() {
  delete m_pObject;
  m_pObject = nullptr;
}

ですが、宣言時に実体で定義することで、newとdeleteは不要となります。

class Hoge {
private:
  Object m_Object = {};
};

もちろん、実行時までサイズが未定となるケースもありますので、求められるゲーム仕様によっては、newが避けられるとは限りません。

ただ「できるだけnewを書かない」という考え方を持っておくと、メモリリークを防ぎやすくなります。

Information

■スマートポインタの活用

C++には 例えば std:shared_ptr という参照カウンタによるメモリ管理が用意されています。これを使うとdeleteの呼び出しが不要なので、メモリリークを減らすことができます。

ただし、参照カウンタの仕組みと落とし穴 (循環参照) について理解が求められます。

Information

■パフォーマンスの改善

newによるヒープ領域の確保は、頻繁に行われるとゲームのパフォーマンスを低下する恐れがあります。

そのためnewの頻繁な呼び出しを避けることで、ゲームのパフォーマンスを向上できるケースがあります。

その他のコードの書き方

重複したコードを避ける

似たようなコードが重複して存在すると、片方に修正が入った際、もう片方の修正が必要となってしまいます。そのため「2つ以上の同じコードを書いているな…」と感じたら、共通関数を作るなどして、処理を共通化することをおすすめします。

Information

プログラムを書いていると、共通処理を書く時間がもったいなくて、共通化は後回しになってしまうことが多いです。

それ自体は問題ありませんが、共通化が必要だと感じつつも実装を先に進めたい場合は「後でやるタスク」といったリストにメモしておいて、作業が一段落したタイミングで「後でやるタスク」の作業を行う…というタスク優先度を管理する方法がおすすめです。

モジュールの共通化

「重複したコードを避ける」という問題に似ていますが、例えば「ダメージ数値の描画」「ボタンUIの制御と描画」といった機能をよく使うゲームの場合は共通モジュール化して処理を使い回れるようにすると、コードの保守性が上がり、仕様変更にも対応しやすくなります。

繰り返し使う式や関数呼び出しはローカル変数にする

例えば以下のコードは繰り返し同じ関数を呼び出しています。

if(GameMgr::GetPlayerHp() < 20) {
	// プレイヤーHPが危険なときの処理.
}
else if(GameMgr::GetPlayerHp() < 0.5f * GameMgr::GetPlayerHpMax()) {
	// HP少ない警告の処理.
}
else if(GameMgr::GetPlayerHp() > 0.8f * GameMgr::GetPlayerHpMax()) {
	// HP安全の処理.
}

そこで現在のHpと最大Hpをローカル変数に代入することで、横に短いコードとなり威圧感を少し減らせます。

int hp = GameMgr::GetPlayerHp();
int hpMax = GameMgr::GetPlayerHpMax();
if(hp < 20) {
	// プレイヤーHPが危険なときの処理.
}
else if(hp < 0.5f * hpMax) {
	// HP少ない警告の処理.
}
else if(hp > 0.8f * hpMax) {
	// HP安全の処理.
}

おしまい

読みやすいC++コードを書く方法でした。

ただC++らしいクラスの書き方やオブジェクト指向の注意点についてはあまり書けなかったので、機会があれば書いてみたいと思います。

Follow me!