プログラムの規模が大きくなると、クラスが思うように分割できず肥大化したり、パフォーマンスをあげるために美しくない構造を選択せざるを得なかったりするなど、様々な問題が発生します。これらの問題はたいてい、すでに誰かが体験し、さらに誰かが良い案を思いついていたりするものです。それをいちいち仕事の度に再発明しいていたのでは効率が悪くてしょうがありませんから、数多く書籍や口コミで広まっているデザインパターンを拾い集めたいものです。
今回はそんなデザインパターンのひとつであるプロキシについて解説します。
CString
{
private:
char *m_pStr;
public:
CString(const char *str);
~CString();
};
CString::CString(const char *str)
{
m_pStr = new char[strlen(str)+1];
strcpy(m_pStr, str);
}
CString::~CString()
{
delete []m_pStr;
}
文字列を管理する CString クラスを考えてみましょう。上記はとりあえず文字列を保存するだけの簡単なものです。
いきなり話が脱線しますが、このクラスは、コピーされるとメモリを破壊するバグを持っています。コピーコンストラクタを定義しなかった場合、コンパイラは自動的にコピーコンストラクタを作成します。これをデフォルトコピーコンストラクタと呼びます。
デフォルトコピーコンストラクタはただメンバ変数をコピーするだけなので、たいていのクラスではクラッシュします。例えば上記の例では、m_pStr をそのままコピーしてしまうので、コピーがデストラクトされるとオリジナルはクラッシュします。これを回避するためには、下記のようにコピーコンストラクタを明確に定義します。基本的にコピーコンストラクタのないクラスは、デフォルトコピーコンストラクタでバグると考えてまず間違いありません。
CString::CString(const CString &str)
{
m_pStr = new char[strlen(str.m_pStr)+1];
strcpy(m_pStr, str.m_pStr);
}
同様に、代入演算子でも同じ問題が発生します。これも代入演算子をオーバーライドしてやればいいのですが、自分で自分に代入するとやはり誤動作しますので、自分(this)にはコピーしないようにチェックする必要があります。
CString &operator=(const CString &str)
{
if (this != &str)
{
m_pStr = new char[strlen(str.m_pStr)+1];
strcpy(m_pStr, str.m_pStr);
}
return *this;
}
最初のクラスでは、オブジェクトをコピーするとデータも必ずコピーされてしまいます。変更が発生するまではひとつのデータを参照するようにしたいときには、どうしたらよいのでしょうか。これを真面目に記述するのは一苦労しそうですが、ラクに解決する方法があります。
データの管理はミニクラスにまかせ、CString はそのミニクラスへのポインタをやりとりすることで、実際にはひとつしかないデータを複数の CString から参照するようにします。このミニクラスを、慣習的にレップ(REP)と呼びます。
また CString はREPに対するプロキシ(PROXY)である、と言います。本当にデータを管理しているのはREPで、CStringはREPの代理人(PROXY)というわけです。
class CString
{
private:
class CRep
{
private:
char *m_pStr;
public:
CRep(const char *str);
~CRep();
const char *str() const
{ return m_pStr; }
};
CRep *m_pRep;
public:
CString(const char *str);
CString(const CString &str);
~CString();
CString &operator(const CStrng &str);
};
CString::CString(const char *str)
{
m_pRep = new CRep(str);
}
CString::CString(const CString &str)
{
operator=(str);
}
CString::~CString()
{
delete m_pRep;
}
CString &CString::operator=(const CString &str)
{
if (this != &str)
m_pRep = new CRep(str.m_pRep->str());
}
CString::CRep::CRep(const char *str)
{
m_pStr = new char[strlen(str)+1];
strcpy(m_pStr, str);
}
CString::CRep::~CRep()
{
delete []m_pStr;
}
上記の例は、とりあえず最初の段階として、先述の CString をそのままプロキシ型に書き換えたものです。ごく単純なプロキシの例で、目的を果たすにはまだ未完成です。下ではこれに参照カウンタを実装していきます。
class CRep
{
private:
char *m_pStr;
int m_nCount;
public:
CRep(const char *str);
~CRep();
const char *str() const;
void AddRef();
void Release();
};
CRep::CRep(const char *str)
: m_nCount(0)
{
m_pStr = new char[strlen(str)+1];
strcpy(m_pStr, str);
}
CRep::~CRep()
{
delete []m_pStr();
}
void CRep::AddRef()
{
m_nCount++;
}
void CRep::Release()
{
if (--m_nCount < 1)
delete this;
}
参照カウンタとは、オブジェクトを参照しているオブジェクトが外部にいくつ存在するのかを、オブジェクト自身がカウントしておくというものです。参照者の増減を記録し、誰からも参照されなくなったら自ら消滅するのです。Windows で DLL を利用したことがある人ならご存じでしょう、COM のアレをクラスでやるのです。
下記は参照カウンタを実装した CString クラスです。CRep は CString の入れ子クラスにしておきたかったのですが、見づらいので独立したクラスにしてみました。
class CString
{
private:
CRep *m_pRep;
public:
CString(const char *str);
CString(const CString &str);
~CString();
CString &operator=(const CStrng &str);
CString operator=(const char *str);
};
CString::CString(const char *str)
{
m_pRep = new CRep(str);
m_pRep->AddRef();
}
CString::CString(const CString &str)
{
operator=(str);
}
CString::~CString()
{
m_pRep->Release();
}
CString &CString::operator=(const CString &str)
{
if (this != &str)
{
str.m_pRep->AddRef();
m_pRep->Release();
m_pRep = str.m_pRep;
}
}
CString &CString::operator=(const char *str)
{
m_pRep->Release();
m_pRep = new CRep(str);
m_pRep->AddRef();
}
CString クラスは、CRep への参照を得るたびに AddRef を呼び出し、CRep に参照していることを知らせます。参照を放棄するときは、Release を呼び出します。CRep::Release の中に delete this; が含まれていることに注意して下さい。CRep は必ず new でインスタンス化されなければならないのです。そして CString は、直接 m_pRep を delete せず、Release メソッドにその管理をゆだねています。
一見危険に見えますが、CRep が外部で使われないように隠蔽しておけば問題はありません。CString はどのような使われ方をしても安全なのです(バグがない限り)。
CString original;
CString *pCopy1, *pCopy2;
pCopy1 = &original;
pCopy2 = pCopy1;
delete pCopy1;
pCopy2->empty();
//確かに参照カウンタを狂わせることには成功したけど、それ以前に何か問題があるんじゃないのか?
もちろん、CString を記述するには、すでに説明したデフォルトコピーコンストラクタやデフォルトコピーオペレータの問題を把握しておかなければなりませんが、これについては始めに説明したとおりです。
プロキシは巨大なメモリを効率よく扱いたいような場合の他にも、内部のデータをリストなどの複雑なポインタリンクで管理したい場合や、特定のハードウェアに対する操作を隠蔽したい場合などにも使えます。その場合、参照カウンタと必ず一組では使わないかもしれません。逆に参照カウンタを使いたい場合は、効率などとは無関係にプロキシを使うことになるでしょう。
いかがでしょう。わりと曲芸的な設計ですよね。この曲芸的な設計はセオリーの1つであり、多くの人が採用しています。その証拠にこの設計はプログラミング言語C++第3版にも掲載されています。
こうした設計のセオリーは、説明を容易にするため、あるいは車輪の再発明を防止するため、デザインパターンと呼ばれている一種のカタログにまとめられています。デザインパターンにあるプロキシには参照カウンタがなく一般化されていますが、代表となるクラスが実態のコピーを管理すると言う基本構造は共通です。
オブジェクト指向言語でプログラミングしている人々の間ではデザインパターンは常識です。今回説明した曲芸的な設計でも、デザインパターンを知っている人であれば、「プロキシパターンで実態に参照カウンタを持たせている」とだけ言えば、わざわざ説明するまでもなく理解できてしまうのです。
今回デザインパターンの一例としてプロキシを取り上げたのは、単にデザインパターンを紹介したかっただけでなく、オブジェクト指向設計の雰囲気を掴むのに非常に適した素材であると思ったからです。
C言語的設計から脱却できないとき、クラス設計がうまくいかないときには、このサンプルの雰囲気を思い出し、デザインパターンの本を見開いて、今作ろうとしているものに最適なパターン名がないか探してみてください。