フリーゲーム・フリーソフトの開発過程を記録していく、TDtechnic公式ブログです。製品はカテゴリの「ダウンロード場」からダウンロードして頂けます。
上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

あけましておめでとうございます。Shiroです。

最近はSIREN動画にも手を出したりしてプログラムがおろそかになってきております。というのも、ライブラリ設計について悩んでいる部分があり、なかなか進める気にならないのです。

その悩んでいる部分というのは、「実装部の隠蔽」です。C言語では、ライブラリ利用者に実装部のコードが見えないように設計すると、インターフェイスの分離が明確になるだけでなく、ソース間の依存性が減り、コンパイル時間も短縮されるというメリットが有ります。具体的には、ヘッダーには関数のプロトタイプと構造体の宣言、typedefなどだけを書いて、関数と構造体の定義や、非公開の関数・構造体は全てソースファイルに書くというものです。

ところがC++のクラスは、この考えにとことん向いていません。C++のクラスは分割定義を許していません。つまり、利用者に見せたいメソッドだけをヘッダーに書いて、メンバ変数やprivateメソッドは.cpp内に書く、といったことができないのです。

なぜこのような制約があるのでしょう。考えられるのは、クラスのサイズを明確にするという目的です。先ほど述べたような分割定義をすると、ヘッダーだけが見える翻訳単位(利用者)と、.cppの翻訳単位(実装者)では、明らかにクラスサイズが異なります。クラス名をClassとすると、利用者にとってはsizeof(Class) = 1でも、実装者にとってはsizeof(Class) = 4だったりするわけです。

これは当然問題になります。もし利用者がClassのオブジェクトを作ると、そのサイズは1です(非virtualメンバ関数しか持っていない場合)。このオブジェクト(またはそのポインタ)を、実装者側で定義された関数に渡すとどうなるでしょう。実装者の方では、Classのサイズが1バイトである保証はありません(メンバ変数などを含んでいるため)。仮に4バイトだったとします。これでは、単なるコピーですらエラーとなります。利用者が作ったClassオブジェクトは実際には1バイトであるにもかかわらず。実装者側では4バイトだと思い込んでコピーを実行するからです。3バイト分は、謎のデータがコピーされてしまうわけです。

先も述べたとおりC++ではクラスの分割定義を認めていないため、上のような事故はまず起こりませんが、とにかく翻訳単位ごとにサイズが異なってはまずいということはおわかりいただけたでしょうか。クラス定義をヘッダーとソースに分けると、どうしてもサイズが異なってしまう問題が発生するわけです。

しかし、ここでちゃぶ台返しをしたいと思います。先の想定では、利用者でClassオブジェクトを生成するとサイズがわからないとうことが問題でした。しかし、それを言うなら最初に述べたC言語流の隠蔽だって、利用者には構造体のサイズがわかりません。ではどうしていたかというと、C言語ではオブジェクトの生成も実装者側で行っていたのです。つまり、生成用の関数のプロトタイプをヘッダーに書いて、実装部でmallocしてポインタを返すのです。そう、ポインタを使うだけであれば、構造体のサイズはおろか、定義すら無くても問題ないわけです。

じゃあC++も同じようにすれば良いではないか?ヘッダーに書いてあるクラスは生成禁止にして、実装部に生成関数を用意すれば良いはずです。正直私はなんでC++はこれができないのか疑問に思います。C言語での例を見ればわかるように、原理的には可能なはずです。「生成禁止」を表すキーワードが必要ですが、それは本質的な問題ではありません。

とはいえ、これだけのC++プログラマーがいて、誰もこの打開案を考えなかったわけではありません。私の知っているものは、「pImplイディオム」を呼ばれるものです。接頭辞pはポインタのこと、Implは実装クラス(Implementation)のことで、つまりpImplはImpl*型のメンバ変数です。以下に例を挙げます:

/* ヘッダー */
class SomeClass
{
class Impl;
public:
/* 公開メソッド */
int Func();
...
private:
Impl* pImpl;
};
/* 実装 */
class SomeClass::Impl
{
/* 本当の定義 */
public:
int Func() { return 100; }
...
};

/* 公開メソッドの実装(中継するだけ) */
int SomeClass::Func() { return pImpl->Func(); }
...

前述の「ポインタで持つことだけを許す」ということができない問題を、「ポインタを持つクラスだけを公開する」という方法で解決したものです。これで、Implクラスのメンバ変数やメンバ関数の定義が変わっても、ヘッダーだけを見ている利用者は再コンパイルの必要がありません。

しかし、私はこれはあまり好きではありません。第一に、中継メソッドを書くのが面倒です。関数から関数へと引数と返り値を渡すだけの簡単なお仕事…あまりやりたくありません。しかも、クラス外で定義する必要があるため、「SomeClass::」などの修飾が避けられず、記述量が増えます。
第二に、クラス間の通信がしにくいです。例えば、SomeClass2のあるメソッドにSomeClassへのポインタを渡すと何かが起こる…ということがしたいとしましょう。SomeClass2を同じ実装者が作っているなら、非公開な関数にもアクセスしたいと思います。ここでいう非公開とは、privateメソッドのことではなく、利用者に見せていないpublicメソッドのことです。つまり、実装者側での操作に使うだけで、利用者には必要ないメソッドです。しかし、そのようなメソッドはSomeClass::Implにのみ定義されているわけですよね。ということは、SomeClass*を受け取ったところで、そのメソッドにはアクセスできないというわけです!結局、このようなことがしたければ、SomeClassにGetImpl()みたな関数を作る必要があるのです(利用者にも丸見え)。

というわけで、私はpImplイディオムをそのまま採用する気にはなれませんでした。そこで、多少なりともこれらの問題を解決できそうなアレンジを私は考えました。それについては次回以降で述べることとして、今回はこの辺で失礼致します。
スポンサーサイト

[2016/01/01 14:25] | (コードネーム:モンジュラ)
|

承認待ちコメント
-


コメント:を閉じる▲
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。