2005-09-19
派生できないクラスについては、
「仮想デストラクタの是非」でも取り上げましたが、ここではもう少し詳しく見ていきたいと思います。
そもそも、派生できないクラスを使えば何が便利なのでしょうか?
派生されないことが保証されていることで、クラスの実装を簡略化できることができますし、場合によっては積極的に効率を向上することができます。
そのクラスが派生されないことが保証されていれば、仮想デストラクタを定義する必要性はなくなりますし、new/delete演算子を多重定義する場合でも、(配列用のものを除けば)固定長のメモリ割り付けで済むようになります。派生クラスで仮想関数のオーバーライドによって破綻しないように注意を払う必要もなくなるわけです。
まず、「仮想デストラクタの是非」では詳しく触れなかった仮想継承を用いて派生できないクラスを実現する方法です。以下のコードを見てください。
class B;
class A
{
A() {}
friend class B;
};
class B : virtual A {};
class C : public B {};
B b; // OK
C c; // エラー
一見して分かりにくいコードですが、これは仮想基底クラスのコンストラクタは、最派生クラスから呼び出されることを利用しています。つまり、クラスAのコンストラクタはクラスCから呼び出されるため、クラスCからA::Aにアクセスできる必要があるわけです。
A::Aはプライベートですから、クラスCからアクセスすることはできません。結果として、クラスBは派生できないクラスになっています。
仮想継承を用いたこの方法の欠点は、やはり理解しにくいということと、仮想継承によるオーバーヘッドです。コンストラクタやデストラクタが内部で行っている目に見えない処理が大きいため、頻繁に構築・解体を行うようなクラスでは、受け入れがたいオーバーヘッドになるかもしれません。
もう一つは、共用体を用いる方法です。共用体は、他のクラスから派生することもできなければ、共用体から派生したクラスを作ることもできません。最大の難点は、共用体はメンバを実質的に一つしか持てないことと、そのメンバ(およびその部分オブジェクト)がコンストラクタやデストラクタを持ってはならないことです。
メンバを一つしか持てないことについては、その唯一のメンバを構造体にすることで解決します。構造体にしておけば、その構造体の中にはどれだけメンバが含まれていても構いません。したがって、コンストラクタやデストラクタを持たないメンバしか必要がない場合は共用体を使うのが一番便利です。
仮想継承を用いた方法に比べれば、共用体の軽量さは魅力的です。そこで、共用体でも、コンストラクタやデストラクタを持つメンバを持たせるという禁忌に足を踏み入れたくなります。以下では、その禁忌についてお話します。
一番簡単な方法は、共用体のメンバをポインタにし、メンバの実態を動的に生成するPimplイディオムを用いる方法です。これであれば禁忌でも何でもなく、ごく普通の方法です。
union foo
{
foo() : pimpl_(new body) {}
private:
class body;
body* pimpl_;
};
しかし、この方法は、組込みでは最も使いたくない動的な割り付けに依存していますし、仮想継承を用いた方法と比べて軽量であるという共用体の利点も失われています。
そこで登場するのが、
「Exceptional C++」
で、
"無謀で、危険で、運がよければ動くかもしれないが、邪悪で、高脂肪、高コレステロール"と酷評されている方法です。以下のコードを見てください。
#include <new>
struct A
{
A() {}
};
union B
{
B() { new(storage_) A; }
B(const B& other) { *get() = *other.get(); }
~B() { get()->~A(); }
B& operator=(const B& other) { *get() = *other.get(); }
A* get() { return reinterpret_cast<A*>(storage_); }
const A* get() const { return reinterpret_cast<const A*>(storage_); }
private:
char storage_[sizeof(A)];
max_aligner aligner_;
};
ここで、max_alignerは処理系の最大の境界調整要求となるPOD型です。具体的には、最大の境界調整要求が32ビットであればlongに定義するなどしておきます。
前述の
「Exceptional C++」
では、このような実装には五つの問題があると指摘しています。
- 境界調整
- 脆弱性
- 保守コスト
- 非効率性
- 単に間違った考え
上記のコードは、これらの問題のうち、1.〜4.については対策したつもりです
*1, *2が、5.についてはどうしようもありません。つまり、プログラマが「何か普通でないこと(=ハック)」をしようとしているという問題点です。
「何か普通でないこと」という観点からすれば、おそらく仮想継承を使った方法もその範疇に入ってしまいます。それどころか、本来C++の言語仕様には備わっていないはずの「派生できないクラス」を作ろうとすること自体が「何か普通ではないこと」でしょう。
もう一点、こうしたハックは組み込み開発では珍しくないということも考慮に入れる必要があります。組み込み開発では、一般的なコーディング手法からすれば、決して普通ではないことが日常的に行われているからです。
できる限り素直な実装の方が好ましいことは言うまでもありませんので、普通ではないトリッキーな実装手法は、他に方法がなくなった場合の秘密兵器として使う方が望ましいでしょう。
*1 swapメンバ関数は実装していませんが、追加するのは簡単です。
*2 クラスAの境界調整要求に対してmax_alignerが大きければ、若干の非効率性が発生します。
Comment