Home日記コラム書評HintsLinks自己紹介
 

フィンローダのあっぱれご意見番 第138回「関係ない話」

← 前のをみる | 「フィンローダのあっぱれご意見番」一覧 | 次のをみる →

最近のパソコンのCPUクロックは3GHzとかいう感じで、 波形を見てみたいものだが、 それだけ速ければ昔は1日かかっていたコンパイルもアッという間に終わる、 かというと、 ビルドしたら終わらないので呆然としているとかいうこともあって、 なぜかコンピュータが速くなっても処理時間が短くなるわけではない、 みたいな現象が見られるようだ。

 

※ クロックは教科書では矩形波となっているはずだが、 3GHz の矩形波というのは想像し難いのである。

処理時間はCPUの速度だけでなく、 周辺機器の速度とか、利用できるメモリの量にも大きく依存するわけだが。 コンパイル待ちの時間に何をしてますか、 みたいな話題が10年前もあったし今もあるというのが面白い。 もっとも、流石に、 待つといっても10年前と今とでは、スケールがかなり違うわけだが。

 

※ Netscape のコンパイルを始めたら何時間待つとか、 いやいや1日待たされるとか、 そういう時代もあった。

§

  

C++ の話。 FPROGORG で話題にしたポインタ技である。 正式には何と表現するのか知らないが、 こんな呼び方は公に広めない方がいいと思う。

 

※ FPROGORG: @niftyにあった濃いプログラマーが集まっていたフォーラム。 もっとも、当時は既に閑散としていたのだが。

まず、 あるクラスが他のクラスの実体をメンバ変数として持っている状況を考える。 それらを ClassAとClassBとすると、 それぞれ独立したファイルに実装して、 ClassA.h、ClassA.cpp、ClassB.h、ClassB.cpp、 の4つのファイルを作るというのが、よくあるやり方だと思われる。 説明に不要な箇所は思い切って、 コンストラクタとか、そういうのまで全部カットしてみたのを List 1 に紹介する。

---- List 1 (ヘッダファイルの一部)----

// (ClassA.h)
class ClassA {
private:
    ClassB m_ClassB;
};

// (ClassB.h)
class ClassB {
public:
    int foo(void);
};

---- List 1 end ----

ClassA の中には m_ClassB.foo() を呼び出す箇所があるのだろう。 ClassA をコンパイルするためには、 ClassB に foo(void); という関数があることを知る必要があるから、 適当な箇所で "ClassB.h" を #include しておく必要がある。

さて、ClassA を使っている処理がどこか別にあるはずだ。 そのような処理が CMyApp というクラス内にあるとする。 このクラスは MyApp.h、MyApp.cpp、という2つのファイルで記述してあって、 ヘッダーファイルはこんな感じ。

---- List 2 (MyApp.h) ----

// MyApp.h
class MyApp {
private:
    ClassA m_ClassA;
};

---- List 2 end ----
  

余談だが、 CMyApp という名前なのに、 なぜ CMyApp.h ではなくて MyApp.h というファイル名なのか? それは私も知りたい。 何でか知らないが、C++の世間の一部ではそういう慣習があるらしいから、 深く考えずに世間に合わせてみた。

 

※ VC的世界というか…。

話は戻る。 MyApp.cpp の中でこうやって ClassA を使うためには、 MyApp.cpp の中で ClassA.h をインクルードしなければならない。 しかし、ClassA.h だけインクルードしてコンパイルすると、 ClassB とは何ですか、 みたいなエラーが出てしまう。 MyApp.cpp からは ClassB の処理を呼んでいないのだから、 MyApp.h と ClassA.h だけインクルードすればコンパイルできそうだが、 実は ClassA.h の中には ClassB というクラスが出てくるから、 ClassA.h を取り込んでコンパイルするためには Class.B.h も取り込む必要があるのだ。 ということで、 結局、MyApp.cpp の先頭は List 3 のようになる。

---- List 3 (MyApp.cpp の先頭) ----

#include "ClassB.h" // ClassA.h をインクルードするのに必要
#include "ClassA.h" // CMyApp 内でこのクラスを使う
#include "MyApp.h"

---- List 3 end ----

ClassA.h をコンパイルするためには必ず ClassB.h もインクルードする必要があるのなら、 ClassA.h の中で ClassB.h をインクルードした方が合理的である。 そうしておれば、ClassB を使ったつもりはないのにコンパイル時に 「ClassB って何ですか」みたいなエラーが出てびっくりしなくて済むのだ。 もっとも、 このようなエラーは修正すれば済むのだから、、 それほど fatal なものではないのだが。

さて、問題はここからである。 ClassB::foo() の処理を仕様変更とかバグ修正ということで、 変更することになった。 foo() の中にごちゃごちゃと処理を追加すると、 ごちゃごちゃして訳が分からなくなりそうだし、 30秒ルール(※1)にも違反しそうだ。 そこで、関数を分割することにした。 ヘッダの対応する箇所も変更することになる。 List 4 のような感じである。

---- List 4 (ClassB の変更) ----
// ClassB.h
class ClassB {
public:
    int foo(void);

private:
    int foo_local(void);
};

---- List 4 end ----

fooの処理の一部を foo_local に分割したのである。 foo_local は ClassB::foo() の中からしか呼ばれない処理で、 だから、private にしてある。 またまた余談だが、 foo からしか呼ばれないということを保証するように書くには、 どうすればいいんでしょうね。 private のメソッドを作ったら、 当然、ClassB の内部のどこからでも呼べてしまうわけで、 それが場合によってはいまいち気に入らないのだが、 だからといって、クラスを分けるというのも大げさすぎるので。 まあ、あまり気にしないのが正解かもしれない。

  

さて、こうやって ClassB.h を変更したら、 ClassB.cpp は当然、再コンパイルする必要がある。 それは仕方ない。 問題は、 ClassA.cpp と、CMyApp.cpp だ。 これらは ClassB.h をインクルードしているから、 ClassB.h を変更したら、 ちまたの開発環境は、自動的に再コンパイルしようとする。 実は、ClassB が変更されたといっても、 内部処理がちゃちゃっと変わっただけなので、 CMyApp クラスの立場で考えたら、 何も再コンパイルするほどのことはないのだが、 多分殆どの開発環境はファイルの生成時刻しか見ていないから、 勝手に再コンパイルするだろう。 結局、こうやって作られた依存関係がこんがらがって、 ちょびッと修正しただけなのに、 殆ど全部のファイルが再コンパイル、なんてことはきっとよくある話なのだ。 とばっちりを受けるクラスが数百個とか数千個という規模の話になってくると、 笑い事ではない。

 

※ 数千個というのは知らないが、 数百個というのは経験したことがあって、 十分にイヤなものだった。

それが何とかならないのか。 と思った人は実はずっと前からいて、 問題も解決しているのである。 具体的にどうするかというと、 ClassA の中で使っている ClassB を、ポインタを使って表現するだけだ。 「だけ」と言っても、ポインタを定義しても中身は勝手に入ってくれないから、 List 5 のように、 コンストラクタで new して、デストラクタで delete するという処理を明示的に書く必要がある。

---- List 5 (ポインタを使う) ----

// (ClassA.h)

class ClassB;

class ClassA {
private:
    ClassB *m_pClassB;
};

// (ClassA.cpp)
ClassA::ClassA() {
    m_pClassB = new ClassB();
}

ClassA::~ClassA() {
    delete m_pClassB;
}

---- List 5 end ----

なお、言うまでもないが、 元のコードをポインタ化する場合には、 m_ClassB.foo(); となっていたコードを、 m_pClassB->foo(); と変更することになる。

はて、さりげなく追加されている最初の行、 class ClassB; って何だろう? これが重要なのだ。 これを書いておくと、 あら不思議、 MyApp.cpp の先頭で、 ClassB.h をインクルードしなくてもコンパイルできるのだ。 もちろん、 CMyApp から直接 ClassB を使っていないという前提が必要だが。

ClassB.h をインクルードしなければ、 ClassB.h の中身を変更しても、 MyApp.cpp を再コンパイルしなくて済む。 どうして ClassB.h をインクルードしなくてもいいのか、 という説明は面倒なので何と今回は省略してしまうのだが、 この話はC++使いならもちろん誰でも読んでいる本、 Effective C++ に出てくるので、 なんでだろ、と思った人はそちらを読んで欲しい。 その本の中では、 もっと積極的にインターフェースと定義を分離する話題として、 ハンドルクラスの紹介とか、そういう所まで踏み込んでいて、 今回やったのは割と大雑把な話である。

§

  

という訳で一見落着というか、 問題解決したみたいだが、 何か一つ気になりませんか。 その new に失敗したら一体どうなるのだろう?

 

※ 「一見」というのはわざとそう書いている。

もちろん、例外が発生するとか、 処理系によっては NULL が返って来るなんていうよく分からない状態になるかもしれないが、 とにかく、もうメモリがないという危機的状態になったら、 何か起こることは間違いない。 だったら、 new した時にそのような処理をしないとプログラム的にマズいんじゃないか、 と思った人もいるだろうか。

  

今回は何とそれも保留しておいて、 オチは別の話だったりする。 ポインタ技を使ったときに new を失敗したらどうよ、 という感覚は正しいと思う。 では、ポインタ技を使わない場合はどうなんだ。 同じように、メモリが足りないという危機的状態になったら、 メンバ変数のための領域を確保できない、という場合もあるだろう。 その場合、一体何がどうなるのだろうか、 とか思った人がいたら、 そう、あなたの感覚も、多分正しい。 でも、それを気にしてコーディングしている人って、 個人的には今まで見た記憶がないのですが。 そういうのは処理系にやってもらうことで、 プログラマーには関係ないのだ、 みたいな割り切りがなぜか発生するから不思議なものだ。

 

※ 30秒ルール: 30秒で理解できない処理は書き直さなければならない。 可読性を上げ、生産性を高める効果があるといわれている。

(C MAGAZINE 2003年12月号掲載)
内容は雑誌に掲載されたものと異なることがあります。

修正情報:
2006-03-02 裏ページに転載。

(C) Phinloda 2003-2006, All rights reserved.