統一したポリシーを決めておかないと一人で書いていても、
コードの修正もリーディングも後々困難になる。
Google C++スタイルガイド 日本語訳も大変参考になる。
ここでは上記に含まれていない点を中心にまとめる。
◆◆◆ 命名規則
コード内の命名規則を決めておく。
● クラス名
最初の一文字目は大文字。
名詞を使う傾向がある。
ただし、抽象クラスは形容詞をつける傾向がある。
(例) Observable
● メソッド、関数名
動詞を当てはめる傾向がある。
Yes/Noのクエリの回答を行う場合は、
Is、Are、Hasといった接頭辞をつける。
(例) IsEnabled()、HssChildren()
否定系は使わない。
(例) IsUnabled()
メソッドではなく自由関数の場合は、
アクションを適用する対象のオブジェクト名を含める。
(例) FileOpen()
● クラス内のメンバ(フィールド)名
"m" + 頭だけ大文字の一文字
(例) mVar
● 配列変数名
配列名は複数形にする。
【例】
book → books
child → children
newした場合にdeleteなのか、delete[]なのかで間違いが起こりにくい。
● ヘッダで利用するインクルードガード名
大文字で記す
#include HOGE_EXAMPLE
名前異空間を使っている場合とそうではない場合には気をつけること。
Hoge::Example
HogeExample
それぞれを区別できるようにしておく。
#include HOGE_EXAMPLE
#include HOGEEXAMPLE
とする。
◆◆◆ 用意しておきたいマクロ
● 0ポインタ
deleteする場合は必ず0を代入する。
目的は、deleteしたポインタを間違って利用しないためである。
0にしておけばアクセスした瞬間に落ちる。
他の値では無効な値でも動き続ける可能性がある。
デストラクタ内であれば変数は後処理され見えなくなるため不要ではと反論もあるだろう。
delete後もnewによって再利用され、new時点での値が残っている可能性はある。
#define SAFE_DELETE(x) { delete (x); (x) = 0; }
改行を入れるならこうだ。
# define SAFE_DELETE(x) {\
delete(x);\
(x) = 0;\
}
配列版も用意する。
#define SAFE_DELETE_ARRAY(x) { delete[] (x); (x) = 0; }
上記を適当なヘッダファイルに記載しておき、
利用するccファイルでincludeしてやればいいだろう。
利用例
T** array = new T*[n];
for (int i=0; i<n; ++i) {
array[i] = new T();
}
for (int i=0; i<n; ++i) {
SAFE_DELETE(array[i]);
}
SAFE_DELETE_ARRAY(array);
◆◆◆ new/deleteポリシー
newされた変数はそのクラスが責任を持って
デストラクタ時にdeleteする。
T::~T() {
SAFE_DELETE(mHoge1);
}
newしたものを渡すときは例外とし、
渡された側がdeleteする。またその旨をコメントしておく。
◆◆◆ メンバ初期化
コンストラクタ時にはメンバをすべて初期化(メンバイニシャライザ)する。
メンバイニシャライザの方が、普通に代入によって初期値を与えるよりも効率的であるためである。
すべての変数の初期化が手間であっても最低限、ポインタ変数は必ず実施し、初期化されていないことによる不可解なバグを回避する。
T::T():
mX(0),
mY(0),
mZ(0) {
~
};
例えば下でinitを呼ぶ前にset()を呼んだらどうなるか。
mXは不定である。
class T {
T() {}
void init() {
mX = new int();
}
void set(int a) {
*mX = a;
}
};
初期値を()で囲むのは、各メンバのコンストラクタを呼んでいるためである。
つまり()の中の値は、コンストラクタに引数を与えていることになる。
int型などの言語に標準で用意されている型(プリミティブ型・基本型などと呼ぶ)であっても、
コンストラクタが存在しているということが推測できる。
◆◆◆ 継承を使う場合の基本設計
● どこで使うか
以下のメリットがある場合に限る。
① 共通部分のくくりだしにより抽象化された全体の性質を見せる時。
例えば、人というスーパークラス(基底)があれば、
男や女のサブクラス(派生)の性差の違いは別にして男や女が
ある程度イメージできる。
② サブクラス(派生)とスーパークラス(基底)がis-aの関係になる時。
共通部分のくくりだしだけに注目して継承関係を結んではならない。
ただし、is-aの関係にあっても、
リスコフの置換原則(LSP:Liskov Substitution Principle)には
従はなければならない。
LSPとは、SがTの派生型であれば、つまりこう、
T(スーパークラス)
S(サブクラス)
T型のオブジェクトが使われている箇所は全て
S型のオブジェクトで置換可能。
以下のような継承関係はis-aであってもLSPに反する。
T(スーパークラス): Ellipse
S(サブクラス): Circle
こういった場合は、合成を利用すべきである(holds-aの関係である)。
合成では相手クラスのパブリックメンバのみに結合するだけである。
③ スーパークラスだけしか知らなくてもいい人に、サブクラスを隠せる時。
人の機能を使いたい場合に男か女かどうかは知る必要はない、ということである。
④ 繰り返しを避けられる時。
● 継承時のトラブル対策
やってしまいがちなトラブルを防ぐためのポリシーを決める。
【概要】
① スパークラス(基底クラス)をnewさせない
② 継承時のdelete忘れ
【詳細】
①
Parentをスパークラス、Childがサブクラスとする
Parent *parent = new Child;
これはあっても、
Parent *parent = new Parent;
このような使い方はしない。
スーパークラスのポインタを通してサブクラスの関数を呼ぶために、
継承させているのである。
ただ、親側をnewすることを機械的に防ぎたい。
親側の関数を子側で再定義し使用する(オーバライドする)ことを
前提として仮想関数として利用する際に、
仮想化させるvirtual付の関数に=0をつけることもできる。
=0をつけた仮想関数は純粋仮想関数というのだが、クラスはnewできなくなる。
これでうっかりミスは防止できる。
1個でもこれがあるクラスを抽象クラスという。
class Parent {
public:
virtual void play() = 0; //中身は持たせない
};
②
スーパークラスのデストラクタにvirtualを付与して仮想関数にして、
継承時のdelete忘れを防止する。
ただし、デストラクタは=0をつけない(純仮想関数にはしない)。
失敗例
Parent* parent = new Child;
delete parent;
Parentのデストラクタしか呼ばれない。
Child内でnewしていた場合は、
Childのデストラクタも呼ばなければならない。
そこで、デストラクタも仮想関数にしてやる。
class Parent {
public:
virtual void play() = 0;
virtual ~Parent() = default;
};
デストラクタに0をつけないのは、親側のデストラクタは中身を持つからである。
スーパークラスのデストラクタにvirtualは必須とすればよい。
子側のデストラクタを呼ぶと親側のデストラクタも呼ばれる。
◆◆◆ 内部でnewするクラスの基本設計
内部でnewするクラスの実体のコピーを防止するために、
コピーコンストラクタと代入演算子は禁止する。
実体を引数にする場合、STLコンテナに入れる場合(これに関しては別観点で後述する)はどうすればよいか。
その時はポインタを使う。
class T {
private:
T(const T&); // コピーコンストラクタの禁止
void operator=(const T&); // 代入演算子の禁止
public:
T() {mX = new int();}
~T() {delete mX;}
};
しかしながらクラスごとに記載するのは手間がかかる。
以下のようなマクロをprivate内で使うと便利である。
#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
TypeName(const TypeName&); \
void operator=(const TypeName&)
class T {
public:
T(int f);
~T();
private:
DISALLOW_COPY_AND_ASSIGN(T);
};
◆◆◆ ポインタ配列と実体配列の使い分け
【どちらを使うべきか】
① ポインタを入れる場合(普通はこうだろう)
T** array = new T*[n];
② 実体を入れる場合
T* array = new T[n];
【性能面からの比較】
①では1個ずつnewしなくてはならない。
毎回newし、その数だけdeleteも必要になってくる。
T** array = new T*[n];
for (int i=0; i<n; ++i) {
array[i] = new T();
}
for (int i=0; i<n; ++i) {
SAFE_DELETE(array[i]);
}
SAFE_DELETE_ARRAY(array);
②ではnewは一度だけでよい。
T* array = new T[n];
for(int i=0; i<n; ++i) {
array[i].initialize();
}
SAFE_DELETE_ARRAY();
ディフォルトのコンストラクタしか呼び出せないため、
initialize()というような(名前はなんでもよい)初期化用の
メソッドを作り読みこませる必要はある。
コンストラクタとは別に初期化関数を呼ぶ手間がかかるが、
newの回数も減り破棄するコードも短くなる。
性能面だけを考えるなら、②を使うべきだろうか。
【安全面からの比較】
ただし下のようなコードを書く懸念があることに留意が必要である。
STLの変数として実体を使うコードである。
class T {
A() {mX = new int();}
~A() {delete mX;}
};
vector<T> gT;
void add() {
T t;
gT.push_back(t);
}
int main() {
add();
return 0;
}
問題
add関数内でのローカル変数としてt作成時にクラスTのコンストラクタが走る。
push_backはコピーコンストラクタが走る。
クラスTのコピーが走る。
明示的にコピーコンストラクタを宣言しなかった場合はコンパイラが暗黙のうちに
コピーコンストラクタを生成している。これをデフォルトコピーコンストラクタと呼ぶのだが、
単にクラスのメンバをそのままコピーするだけである。
つまりメンバ変数mXもコピーされる。このポインタ変数がコピーされてしまうと
同じ場所を指すポインタが、コピー元とコピー先とで2つ存在することになる。
add終了時にローカル変数として作ったtのdeleteが走り、コピー元が消え、
コピー先でもあるmXも使えなくなる。
簡単なコードでもこういったトラブルは起きる。
性能を犠牲にしても安全面を優先するという選択も判断材料にしてほしい。
ちなみにこの対策としてすぐに2つ思いつく。
・中でnewする型はSTLコンテナへはポインタを入れる
・内部でnewするクラスはコピーコンストラクタと代入演算子は禁止
◆◆◆ 内部でnewするクラスの基本設計で実施した通りである。
◆◆◆ 関数のパラメータは値かポインタか参照か
値渡しの場合はいいだろう。
パフォーマンスの懸念がなく書きかえもしないようなデータを渡す時である。
考慮が必要なのはポインタか参照かということになる。
実質両者は同じものであるが、
参照にメリットがあるとすれば、
構文がシンプルになる、
NULLにならない、よってNULL値チェックがいらない、
といったメリットがあるだろうか。
以下を見てみる。どの変数が書き換えられるか分かるだろうか。
func(a, b, c, d, e, f);
書き換えない変数はconstの付与を徹底すればいいのではと思うだろう。
func(型* a, 型* b, 型* c, const 型& d, const 型& e, const 型& f)
しかし、それでは関数内で利用する関数で分からなくなる。
func1(型* a, 型* b, 型* c, const 型& d, const 型& e, const 型& f) {
func2(a, c, e);
func3(b, d, f);
}
ヘッダを見ればよいだろうが、それではヘッダを見る手間がかかる。
そこで、書き換えないなら参照(もちろんconstをつければなおよし)、
書き換える場合はポインタ渡し、
というルールにしてもいいかもしれない。
func(&a, &b, &c, d, e, f);
dとeとfは入力であり、aとbとcが出力であることが一目瞭然である。
ただしこれでは管理工数がかかると思うかもしれない。
関数の引数は書き換えられる可能性があると常に頭に入れておく、
ぐらいの緩いポリシーの方がいい場合もあるだろう。
◆◆◆ 関数、メソッドからの戻り値に関して
関数、メソッドからの戻り値にポインタか参照、どちらを使うべきだろうか。
まずはポインタ返しと参照返しの例を見てもらう。
① ポインタを使う場合
class T {
public:
A* getA() const { return &a; }
private
A a;
};
A *a = t.getA();
② 参照を使う場合
class T {
public:
A& getA() const { return a; }
private
A a;
};
A& a = t.getA();
もしくは
A a = t.getA();
ただし、後者は代入時に値のコピーが走る。
Aが大きなクラスならそのコストがかかる。
知らないうちに遅いコードを書いていたということが起きるかもしれない。
①の場合にコピーが発生する場合は、こういったコードを書いたときだろう。
A a = *t.getA();
関節参照演算子である*を使うことで
その指している先が利用されることが明示的にわかる。
結論だが、どちらでもいい気がするが、
*をつける手間はあるが、何をしているのかはっきりする
①の方針をとることが多い。
◆◆◆ ヘッダのポリシ
【概要】
① インクルードガードの付与
② using namespaceの不宣言
③ パブリックなヘッダとプライベートなヘッダの使い分け
④ すべてのヘッダは自分が依存している他のヘッダファイルをインクルード
⑤ ccファイルからのインクルードは必要なものだけをインクルード
⑥ ヘッダのパス名のルール
【詳細】
① インクルードガードの付与
インクルードガードは基本だろう。
// example.h
#ifndef EXAMPLE_H
#define EXAMPLE_H
class Example {
・
・
・
};
#endif
② using namespaceの不宣言
usingキーワードを使うことで名前空間内でシンボルを簡単に使うことが可能となる。
using namespace std;
stdライブラリであれば、std::coutをcoutと略して使える。
Hogeというnamespaceにいれば、
Hoge::cout
そうではければグローバルな名前空間として
::cout
と区別をつけることはできる。
ただし、ヘッダ中でusing namespace宣言を多用すると、
これを参照したファイルもグローバル名前空間に見えてしまい、
名前空間の意味がなくなってしまう。
特にAPIとして提供するようなパブリックヘッダでは厳禁とする。
プレーンなCでよくやるように、一意な接頭辞を使う方法もあるだろう。
③ パブリックなヘッダとプライベートなヘッダの使い分け
パブリックなヘッダはクラスの顔であるべきであり、内部事情をさらさない。
コンパイラにとって優しいだけではなく、人にとってもその方がいいからである。
④ すべてのヘッダは自分が依存している他のヘッダファイルをインクルード
b.hで定義されている型をa.hで使っていた場合、a.hの冒頭で下を書く。
#include "b.h"
そうしないと、a.hをインクルードする側に、b.hのインクルードも強要することになり、
管理が煩雑になる。
a.hを使うccファイル側に下のようなことはしたくない。
#include "a.h"
#include "b.h"
そもそもインクルードが不要で前方宣言だけでいい場合がある。
クラスAがクラスBのヘッダのインクルードが不要となる例を見てみる。
まず、クラスBを前方宣言。
class B;
class A {
B* b;
};
・変数の実体を関数の引数に使う
class A {
void setB(B);
};
・変数の実体を返り値に使う
class A {
B getB();
};
・テンプレートを使う場合
class A {
Hoge<B> b;
};
クラスAがクラスBのヘッダを必要とするのは、
そのクラスのオブジェクトをデータメンバとして利用する場合、
そのクラスから継承する場合のみである、ということである。
⑤ ccファイルからのインクルードは必要なものだけをインクルード
④を徹底していれば特に気にすることはない。必要なものをインクルードすればよい。
⑥ ヘッダのパス名のルール
ディレクトリを掘った場所にあるヘッダをccファイルから読み込む場合は
インクルードパスからのパス名も記載する。
#include "hoge/piyo/example.h"
ccと同じディレクトリにあれば当然パスは不要である
しかし規則を統一させたいためこうする
また同じ名前のヘッダ名があった場合に間違いを防げる
◆◆◆ キャスト
型変換するキャスト時にはC++で用意されている型変換方法をとる。
つまり、下のようにはしないということである。
float a = (int)b;
どういうキャストなのか明示させる目的と、
検索しやすくするためである。
【概要】
① static_cast
intやfloatなどの相互変換。
② reinterpret_cast
ポインタ型同士の変換や、int型の変数をポインタ型に変換する場合など。
利用するのはバイナリを読み書きする時ぐらいだろうか。
③ dynamic_cast
ダウンキャスト時。
④ const_cast
constを外すような設定はしない。
忘れてよい。
【例】
①
int num;
double f = 12.3;
num = static_cast<int>(f);
②
クラスTがあったとして、intの数字をポインタに変換する。
int num = 12345;
T* p = reinterpret_cast<T*>( num );
当然だが、どこを指すか分からない値をアドレスとして使うことはできない。
このキャストはバイナリの読み書きぐらい時にしか使わない。
いい例を見かけたのでそれを用いて説明する。
ある画像データには12バイト目から4バイトに画像の高さ情報が格納されている。
この画像フォーマットでは高さを表す型はunsignedと決まっている。
unsigned *height;
画像データを1バイトずつ読みたいためchar型のimageData変数に取り込む。
char* imageData;
この画像データから高さ情報を抜き出してみる。
height = reinterpret_cast<unsigned*>(&imageData[12]);
しかしこれではまずい点が2つある。
heightのunsigned型が4で割り切れるアドレスを持っている保障はない。
ある変数を最初からunsigned型とした作った場合はコンパイラが4で割れる場所に置いてくれる。
char型はその限りではない。仮に偶然char型が4で割れる場所に格納されても、
例えば勝手に7文字分目からを読んでunsignedに変換したらそれは4では割り切れないだろう。
10進数で言えば、1234という数字はそのままでは解釈できないため、
1と2と3と4に分けて読んで合成することになる。
4 + 3*10 + 2*100 + 1*4000 = 1234 // リトルエンディアン例
1 + 2*10 + 3*100 + 4*4000 = 4321 // ビッグエンディアン例
2つの例を書いたのは、合成するにあたって
エンディアンがリトルエンディアンなのかビッグエンディアンなのか
考慮しなければならないためである。
ここでの例として使った画像データがリトルエンディアンの場合は
下のように書き換えるべきである。
コンピュータは8ビットをまとめたバイト単位でしか読み書きできない。
ただ、実際は4バイト単位で操作する。
2の8乗である256進数の数字が4つ並んでいるイメージである。
unsigned height = getUnsigned(&imageData[12]);
unsigned getUnsigned(const char+ p) {
const unsigned char* up;
up = reinterpret_cast<const unsigned char*>(p)
unsigned ret;
ret = up[0]
ret |= (up[1] << 8);
ret |= (up[2] << 16);
ret |= (up[3] << 24);
return ret;
}
②の説明が長くなった。
③、④は割愛。
◆◆◆ unsignedの使いどころ
正の整数であればunsignedでいいのだろうか。
その変数が正であっても減算処理内で計算結果が机上で負数にならないことを
考慮できればよいがそれを考慮するのも手間である。
そこで次の指針を立てる。
① ビット演算をする場合
② intでは足りない範囲で必ず正になる計算処理をする場合
上を考えたくなければすべてintでいい。
または富豪的プログラミングをするのであればdoubleを使う。
◆◆◆ インライン化の方針
コンパイル時に関数の呼び出し側に関数をコピーし、
呼び出しのコストを抑えるというメリットがあるが基本使わない。
理由はインライン化するには関数の中身が見えなければならず、
同一ccファイルでしか使えないため利用用途が限られる。
ccファイルから別のccファイルをインクルードなどしない。
コンパイルはccファイル頃にバラバラに行うのが普通だ。
ヘッダに関数の中身を書く方法はどうだろうか。
ヘッダに書かれた関数は自動でインライン化される。
しかし、ヘッダ内に関数の中身は書くことも推奨し難い。
利用者に不要な中身を見せない、という方針に反する。
またその関数を変更した場合、このヘッダを読み込む全ccファイルのコンパイルが走る。
また、virtualがつく仮想関数では使えない。
実行するまで分からないので仮想なのだから当然だ。
上記デメリットを上回るなら利用してもいいだろうが、
人が考えるよりコンパイラの最適化に任せた方が楽だろう。
◆◆◆ グローバル変数を利用しない
例えば、あるクラスのインスタンスをグローバル変数に代入して
使いたくはないが、引数に延々と渡していくようなこともしたくない。
そこでシングルトンを使う。
あるクラスのインスタンスがプログラム全体で
1つしか存在しないことも保証できる。
※シングルトンをモノステートと誤解ないように。
シングルトン:1つしかインスタンスを作成させないことによる単一構造の強制
モノステート:複数のインスタンスで同じデータを強要する単一動作の強制
class T {
public:
// 唯一のアクセス経路
// static関数として定義する
static T* getInstance() {
return msInstance;
}
// create()内でコンストラクト
static void create() {
if( msInstance == 0 ) {
msInstance = new T();
}
}
// destroy()内でデストラクト
static void destroy() {
delete msInstance;
}
void example() {
~
}
private:
// コンストラクタを隠す
// 勝手にnewさせない
// create()を通すことを強制する
T() {}
// デストラクタを隠す
// destroyすればdeleteできるが、
// 意図せずdeleteされない備えにはなる
~T() {}
// コピーコンストラクタを隠す
T( const T& );
// 代入を隠す
T& operator=( const T& );
private:
static T* msInstance;
};
使い方もまとめておく。
クラスポインタであるmInstance変数を作る。
static指定子をつけた静的メンバ変数なので、
メモリ上にただ1つしか存在しない。
T::create();
メソッドを呼び出してみる。
T::getInstance()->example()
少し長く感じるなら別名をつければよい。
T *t = T::getInstance()
t->example();
破棄する場合はdeleteではない。
t->destroy();
余談だが、グローバル変数をファイル間で使っている場合、
その変数をexternして実体は別にいることを宣言することがある。
extern指定子をグローバル変数以外に使うことはない。
つまりシングルトンを利用すればexternは不要である。
◆◆◆ 煩雑なif文の忌避
C++のコードに限らないが、煩雑なif文は避ける。
【変更前】
if ((A == ~ && B == ~) || (B == ~ && C == ~) {
~
}
【変更後】
book isA = (A == ~);
book isB = (B == ~);
book isC = (C == ~);
bool condtion0 = (isA && isB);
bool condtion1 = (isB && isC);
if (condition0 || condition1) {
~
}
◆◆◆ 配置new(placement new)の利用
大量にnewしなければならず、速度も求められる場合には
メモリプールを使ってもいいだろう。
T* memoryPool = operator new(sizeof(T) * n);
for (int i=0; i<n; ++i){
new( &memoryPool[i] ) T; // 配置new
}
for(int i=0; i<n; ++i) {
array[i].~T(); // デストラクタの呼び出し(ここではメモリ解放は不要)
}
operator delete(memoryPool)
例えば、newはclass内にも定義できるので、
そこから配置newを使う場合は、グローバルスコープの配置newが確実に
呼ばれるように、スコープ解決演算子を付与してもいいかもしれない。
::new( &memoryPool[i] ) T;
◆◆◆ シングル引数を取るコンストラクタの宣言時のexplicitキーワード
C obj = 10;
は
C obj(10);
に変換される。
C::operator=(int)
が呼び出されるのではない。operator=()が定義してあっても同様である。
非明示的な振る舞いは混乱を招く。
非直感的な自動変換を禁止するため、
explicit C(int)
と宣言する。