順調に動いていたはずのプログラムが突然ダンプを吐いてクラッシュする・・・。
まさに悲劇としか言えません。
ある調査機関によると成人男性の約89%がこの不幸に出会っているという報告もあります。[要出典]
最も多く、そして最も簡単に発生する原因としてnullポインタアクセスが挙げられます。以下のコードを見てみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// アクセルしか無い車。 ※リコールの対象になる恐れがある class Car { public: void accel() { speed_ += 1; // ガンガン加速する } private: int speed_; }; int main() { Car* car = nullptr; car->accel(); // nullptrに対してメソッドを呼ぶ。当然事故る return 0; } |
このプログラムは実行するとすぐさまクラッシュします。当然ですね。
問題はどこでアクセスエラーが発生するか?です。
正解は5行目の
1 |
speed_ += 1; |
の部分なのですが、なぜメソッド呼び出し
1 |
car->accel(); |
ではなく、その内部の処理まで進むのでしょうか。
理由はc++のアーキテクチャにあります。
あるクラスのメソッド、あるインスタンスのメソッド呼び出しとは、内部的には通常の関数にインスタンスのポインタを渡しているだけなのです。暗黙の引数としてthisが渡されるイメージです。
ですので、先ほど提示したコードは、C言語的な擬似コードで書くと
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct Car { int speed_; }; void accel(Car* this) { // メソッドとは関数とインスタンスのセット this->speed_ += 1; } int main() { Car* car = nullptr; accel(car); return 0; } |
というコードに近いイメージになります。
これならアクセスエラーが発生する場所が
1 |
this->speed_ += 1; |
だという事がよりイメージしやすいのではないかと思います。
C++でコーディングしていると、ポインタを操作する前にnullチェックをするコードを書くケースが多いのですが、上記を踏まえ、こんなことを考えるかもしれません。
そうだ!メソッドの中でチェックしよう!
1 2 3 |
if (car) { car->accel(); } |
と書くかわりに
1 2 3 4 5 6 7 8 |
void Car::accel() { // まずメソッドの頭でnullチェックする if (!this) { return; } speed_ += 1; } |
こんな感じでメソッドの中でチェックすれば、呼び出し側が、色々な場所で呼び出すかわりにnullチェックしなくて済む。と思うかもしれません。
しかし、このコードは再びアクセスエラーになる可能性があります。
なぜか?
一見、ちゃんとnullチェックもしているように見えます。
C++の規格ではメソッド中でthisがnullではないことが保証されているため優秀なコンパイラは単にメソッド内でのthisへのnull判定コードを最適化で消し去ります。
これが今回のアーティクルのタイトルにもなっているメソッド内でのthisの最適化です。
コンパイラオプションによってこの最適化を抑制することもできるのですが、規格に準じたコードではないので書くべきではありません。
コラム
行儀よく普通にコーディングしてるとメソッドの中でthisを判定したいことは少ないと思います。こんな最適化に関する知識、それほど重要じゃないと思われるかもしれません。
あるゲームエンジンを書いていた時のことです。
そのゲームエンジンはオブジェクトを生成すると同時に色々な準備を同時に行う必要があったのでファクトリによって生成されました。生成と破棄の詳細についてユーザは知る必要がないのでインスタンスを破棄する時、ユーザーは単に破棄用のメソッドを呼びます。
1 2 3 4 5 6 |
// 生成 内部でメモリ割り当てからモデルの生成等ゲームの世界で使う準備をする. // メモリ割り当てがnewで行われたかmallocで行われたか呼び出し側は知らない GameObject* obj = GameObject::create(); // 破棄 生成時と同様、その内容は知る必要がない obj->destroy(); |
不特定多数のゲームプログラマが使うようなシステムを実装する場合、ゲームエンジンを書いているプログラマは「とにかく簡単に完結に」 という心理になります。
delete演算子にnullポインタを渡しても支障が無いので、この破棄処理もobjがnullかどうか破棄の前に判定させることを強要しない作りにしたほうがシンプルで良いと考えるのです。
そして……メソッドの中でthisによる判定を書いてしまうことが……あるのです……。
あなたがもし、ゲームエンジンの実装を任されているエンジニアで、なおかつdeleteがnullポインタを許容する事実を知っている場合は、この最適化の事を覚えておいて損はないでしょう。
そして、もしよりシンプルでブレの無いエンジンを目指すなら、そもそもオブジェクトの生成はスマートポインタで返すべきかもしれません。(ハードスペックと使っているコンパイラが許すなら)
1 |
std::shared_ptr<GameObject> obj = GameObject::create(); |
まとめ
- メソッド中ではthisがnullじゃないことは規格で保証されている
- 故にメソッド中でのthisそのものに対する判定は最適化される場合がある
- コンパイラオプションで抑制できるとしてもメソッド内でthis判定するのは規格外なので良くない