仮想関数を持たないクラスの面白い使い方

かな~り昔に書いた記事の中でDirectShowを使う(2)で、CRefTimeというヘルパクラスを使用すると変換がやりやすい、と言うことを書いたのですが、この記事に関連しています。

仮想関数を持たないクラス、というのはメモリ配置上、構造体などと同じ見方が出来ます。これを使うとちょっと楽しいことが出来ます。

CRefTimeの使い方

CRefTimeはDirectShowで時間単位の変換をやりやすくしたクラスで、内部的には以下のように実装されています。(正確には実装を模したものです)

class CRefTime{
public:
	REFERENCE_TIME m_time;
	CRefTime() { m_time = 0; }
	CRefTime(LONG msecs) { m_time = MILLISECONDS_TO_100NS_UNITS(msecs); }
	CRefTime(REFERENCE_TIME rt) { m_time = rt; }
	inline operator REFERENCE_TIME () const { return m_time; }
	   inline CRefTime& operator = (const CRefTime& rt) { m_time = rt.m_time; return *this; }
	inline CRefTime& operator = (const LONGLONG ll) { m_time = ll; return *this; }
	inline CRefTime& operator += (const CRefTime& rt) { m_time += rt.m_time; return *this; }
	inline CRefTime& operator -= (const CRefTime& rt) { m_time -= rt.m_time; return *this; }
	inline LONG Millisecs(void) { return (LONG)(m_time / (UNITS / MILLISECONDS)); };
	inline LONGLONG GetUnits(void) { return m_time; }
};

つまるところ、REFERENCE_TIME型の変数が一つと、それをサポートするためのメンバ関数群で成立しています。

こういうクラスがあるなら時間管理にCRefTimeを使用したいところなのですが、DirectShow本体では通常REFERENCE_TIME型を使用しているのでそうはいきません。

まあ、CRefTime型を宣言してメンバのm_timeのアドレスを参照すればそれで済むことなのですが・・・。

でも、これって・・・

しかし、このCRefTime、メモリ配置を考えてみると・・・。

クラスを宣言したとき、(コンパイラにもよりますが、だいたいのコンパイラでは)

  • メンバ関数そのものはthisポインタを暗黙の引数とするただの関数である(thisポインタが第一引数+クラス名の名前空間内にある関数と同じ)
  • 仮想関数を持たないクラスは、メモリ上メンバ変数だけを持つ構造体と同じ
  • 仮想関数を持つクラスは、メモリ上メンバ変数の領域と仮想関数テーブルと呼ばれる領域がある構造体と同じ
  • 継承があっても、継承元およびそれ自身に仮想関数を持たないなら、メモリ上は継承元のすべてのメンバ変数+それ自身のメンバ変数を持つ構造体と同じ

となります。

つまり、単にREFERENCE_TIME型の変数が一つしかないのであれば、メモリ上は64bitの整数変数がある、という扱いにしかなりません。

そうすると、こんな書き方も出来ることになります。

REFERENCE_TIME t;
m_lpMediaSeeking->GetDuration(&t);
LONG lMediaTimeOnMS = ((CRefTime *)&t)->Millisecs();

これがCRefTimeをヘルパクラスとして使った書き方の一つになります。

CRefTimeを素直に使ってもいいですが、こういう書き方もある、と言うことで・・・。

この考え方は他のヘルパクラスにも使える

と言うわけで、あるクラスCHelperに対して

  1. クラスCHelperにあるメンバ変数はただ一つである(基本変数型である必要は必ずしも無い。構造体一つのみや共用体一つのみでも可)
  2. クラスCHelperには仮想関数を実装していない
  3. クラスCHelperは継承をしても良いが、継承元のクラスには仮想関数が存在しない。
  4. クラスCHelperが継承を行ったとき、継承元のメンバ変数の数とクラスCHelperにあるメンバ変数は足しても一つだけである(「継承元にメンバ変数が一つ+CHelperにメンバ変数はない」もしくは「継承元にメンバ変数はない(ヘルパ関数群クラス)+CHelperにメンバ変数が一つ」)

という場合に限り、そのクラスに存在しているメンバ変数の型と同じ変数型の変数に対して

class CHelper{
protected:
	value_type m_value;
public:
	・・・
	inline int GetInteger(void){ ・・・ }
};
class CHelperEx : public CHelper{
public:
	・・・
	inline float GetFloat(void){ ・・・ }
};
value_type val; int intval; float fltval;
intval = ((CHelper *)&val)->GetInteger();
fltval = ((CHelperEx *)&val)->GetFloat();

という使い方が出来ます。面白いテクニックですね。

C++では必要がない物を仮想関数にしないようにした方が良いかも

一つでも仮想関数が入ると

  • 対象のクラスのメモリサイズが増加する
  • クラスをコピーするときに単純なメモリコピーでのコピーが出来なくなる。そのため、コピーにはコピーオペレータが必要になる。
  • 仮想関数を呼び出すときに仮想関数テーブルを参照する必要が出てくるので微妙に遅れる
  • 仮想関数はインライン化といった高速化テクニックがほとんど使用できなくなる

と言う現象が起こります。

特にCRefTimeのようなヘルパクラスでは仮想関数はまず使わない方がいいと思います。

普通のオブジェクトクラスなら仮想関数を使うことで実装がしやすくなったりするのですが・・・。

ちなみに類似した項目として

共用体で、最大の要素が整数型で表すことが出来るときにポインタを通してキャストが可能、と言う物があったりします。

一番よく使う例がLARGE_INTEGER構造体で、LARGE_INTEGER構造体を宣言したくないときに

LONGLONG pos,nowpos; pos = 0;
SetFilePointerEx(hFile,(*(LARGE_INTEGER *)&pos),(LARGE_INTEGER *)&nowpos,FILE_CURRENT);

と言う方法で間接的にLARGE_INTEGER構造体を使う、と言うことも出来ます。

あまりこんなことはしないと思いますが、例としてですね。


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

この記事のトラックバック用URL