[C++] モジュールとクラスと(ヘッダファイルについてのその後)

こないだ、クラスのprivateな要素もヘッダファイルで定義しないといけない、
ということを書きました。今回は、その後です。

結論から言うと、公開クラスと実装クラスを分けることで、
privateなものを隠すことができる、ということになります。

このやり方自体は、デザインパターンかなんかで見た記憶があります。
しかし、その時は「実装クラスを間にかませるのは、クラス構成の見通しが悪くなる」
と思って、あまりピンときませんでした。

このごろやっと、ご利益がわかってきました。


1.ヘッダに直書き

まず、たとえば
[my_string.hpp]
class MyString
{
    char a[100];
public:
    MyString(char* c_str){ /*...*/}
    bool isEmpty(){/*...*/}
};

というクラスをすべてヘッダファイルmy_string.hppで定義したとします。
(クラスの例が悪くてすみません;)
この場合、クラス全体の内容が見やすく、
メソッドや属性を追加しやすいというメリットがあります。
しかし、変更を加えるとincludeするすべてのソースファイルを
コンパイルしなおさなければならないという、厄介なデメリットがあります。

私の場合は、あまり褒められた書き方ではないのですが、
設計が流動的な初期の間はこのようにヘッダに直書きしています。

しかし、だんだんコンパイルにかかる時間が長くなると、
このやり方には耐えられなくなります。

2.ヘッダとソースのペア

そこで、ヘッダのメソッドを定義から宣言にし、
対応するソースファイルをつくります。

[my_string.hpp]
class MyString
{
    char a[100];
public:
    MyString(char* c_str);
    bool isEmpty();
};

ソースファイルをつくって、メソッドの定義を記述します。
[my_string.cpp]
#include "my_string.hpp"
MyString::MyString(char* c_str)
{
    /*...*/
}
bool MyString::isEmpty()
{
   /*...*/
}

この場合、my_string.cppは単独でコンパイルし、
できたオブジェクトファイル(my_string.o)をリンクすることで実行ファイルを作ります。
おもなメリットは、コンパイル単位が分割されて、コンパイル時間が節約できることです。
(メソッドがインライン化されないなどもありますが……)

しかし、メソッドや属性に変更を加えるためには、
2つのファイルをメンテナンスしなければならないというデメリットがあります。

3.ソースファイルの中に実装クラスを作る

そこで、実装クラスに分離してみます。

[my_string.hpp]
class MyStringImpl
class MyString
{
    MyStringImpl* mp_impl;
public:
    MyString(char* c_str);
    ~MyString();
    bool isEmpty();
};

実装クラスは宣言だけにしてポインタを持ちます。

[my_string.cpp]
#include "my_string.hpp"
class MyStringImpl
{
    char a[100];
public:
    MyStringImpl(char* c_str){ /*...*/}
    bool isEmpty(){/*...*/}
};

MyString::MyString(char* c_str)
        :mp_impl(new MyStringImpl(c_str){}
bool MyString::isEmpty(){ return mp_impl->isEmpty();}

公開クラスMyStringのメソッドを実装クラスMyStringImplに委譲してます。

これで、はじめの直書きのメリットと、
分割コンパイルのメリットを両取りできるわけです。
クラス階層もMyStringImplはmy_string.cppのソース内でしか使わないので
外側から見れば思ったほど複雑化しません。

依然としてMyStringのインターフェースの変更は手間ですが、
MyStringImplの実装用の非公開メソッドや属性は簡単に変更できます。


この方法でも、クラスを利用するためにはオブジェクトファイルへのリンクが必要なので、
ライブラリのようなincludeだけで利用できる、という形にはできませんが、
アプリケーションロジックのためのクラスでは結構使えるという実感です。
粒度の大きくてメソッドの処理が複雑なクラス(処理が主眼のクラス)
ではかなり使えると思います。
(逆にいえばこの例のような小さなクラスでは、あんまりご利益はない)

インターフェースクラスMyStringごとにモジュールを区切っている、
そんな感じです。ではまた。