pimpl の性質
pimpl のテクニックを利用した【オブジェクトの隠ぺい】は、ポインタの特徴を利用したものだ。
多くの場合、以下のような形で実装されている。
内部実装クラス(隠ぺいしたいクラス)
class CatImpl
{
public:
void Naku() { printf( "鳴いたのは zu 回目です。\n", naku_count_++; ); }
public:
virtual ~CatImpl();
private:
// 隠ぺいしたい変数
size_t naku_count_ = 0;
};
公開クラス
ヘッダ
class Cat
{
// 隠ぺいしたいクラスをポインタで参照する
using Impl = std::unique_ptr<CatImpl>;
public:
// 公開インターフェイス
void Naku();
public:
// ctor
Cat();
private:
const Impl impl_;
};
実装
void Cat::Naku()
{
return impl_->Naku();
}
Cat::Cat()
: impl_( std::make_unique<CatImpl>() )
{
}
この方法をよく見てみると、Impl クラスにヒープ領域を割り当てることが前提とされたコードとなっていることがわかる。公開クラスのインスタンスを生成する頻度やサイズによっては、この挙動が望ましくない場合もある。(ヒープ領域の消費に伴う問題として、メモリフラグメンテーションは誰もが知るところだろう。)
Implクラスに割り当てる領域について考える
単刀直入にいうと、公開クラス側にImpl クラスを配置するメモリ領域を持たせてしまおう、というアイデアを試してみる。
ImplContainer
公開クラスがImpl クラスを保持すると同時に、その配置領域を提供できるようにするためのヘルパクラスを定義する。
class Impl
{
public:
virtual ~Impl() {};
};
template<typename T, size_t N = 256>
class ImplContainer
{
struct lazy_deleter
{
void operator()( T* p ) const { static_cast<Impl*>(p)->~Impl(); }
};
using InstanceHolder = std::unique_ptr<T, lazy_deleter>;
using InstanceBuffer = std::array<uint8_t, N>;
public:
template<typename... Args>
static auto GetCreator( Args&&... args )
{
return [&]( InstanceBuffer& buffer )
{
return InstanceHolder( new ( buffer.data() ) T( std::forward<Args>(args)... ) );
};
}
ImplContainer( const std::function<InstanceHolder(InstanceBuffer&)>& f )
: holder_( std::move(f( buffer_ )) )
{
}
const InstanceHolder& operator->() const
{
return holder_;
}
private:
InstanceHolder holder_;
InstanceBuffer buffer_;
};
解説
InstanceHolder
using InstanceHolder = std::unique_ptr<T, lazy_deleter>;
Impl クラスを保持するためのポインタとして機能する。型Tは、Implクラスを継承した任意のクラスをとる。std::unique_ptr と同等の仕事をするが、deleter を書き換えてある。これは、deleteの呼び出しを抑制し、デストラクタの呼び出しだけを行いたいからである。
InstanceBuffer
using InstanceBuffer = std::array<uint8_t, N>;
Impl クラスを実際に配置するメモリ領域となる。公開クラスを定義する時点では Impl クラスの正確なサイズを知ることはできない。そのため、固定長サイズの指定で事前に決めておく必要がある。
コンストラクタ
ImplContainer( const std::function<InstanceHolder(InstanceBuffer&)>& f )
: holder_( std::move( f( buffer_ ) ) )
{
}
Impl クラスの正確なサイズをとることができない理由と同じく、ImplContainer のコンストラクタでは、InstanceHolder に直接インスタンスを構築することができない。そのため、Impl クラスのインスタンスを生成するための関数を受け取るようにしておく。そして、その関数にInstanceBuffer を渡し、placement new によりインスタンスを構築させる。
適用
ImplContainer を使って、Cat および CatImpl クラスを定義しなおしてみる。
内部実装クラス(隠ぺいしたいクラス)
class CatImpl : public Impl // <=== Impl を継承させる
{
public:
void Naku();
virtual ~CatImpl();
private:
size_t naku_count_ = 0;
};
公開クラス(ヘッダ)
class Cat
{
public:
void Naku();
Cat ();
private:
const ImplContainer<CatImpl> impl_; // <=== ImplContainer
};
公開クラス(実装)
Cat::Cat()
// ↓=== 生成関数を渡す(関数は ImplContainer が提供してくれる)
: impl_( ImplContainer<CatImpl>::GetCreator() )
{
}
実例
int main()
{
{
Cat cat; // <=== CatImpl はスタック上に構築される
cat.Naku();
// <=== スコープを抜けるときに、Cat のデストラクタが呼ばれる
}
return 0;
}
2種類の最適化
Impl を使った記述法は、インターフェイスを洗練させるためにとても有効な手段だった。これはコーディング行為そのものにおける最適化となる。次はコーディングされたプログラムが実際に動作したらどういった影響が出るのかについて深く考察してみる必要がある。つまり、 パフォーマンスの最適化だ。それはプログラム動作の安定性を保証するためには欠かせない技術といえる。
よいプログラムとは、これらのどちらも欠けていないと思えるものだろう。日々精進…。