5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

C++ の小さな技術を紹介するシリーズ【小技C++ 全9回】#7<続・オブジェクトの隠ぺい>

Last updated at Posted at 2019-08-13

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 を使った記述法は、インターフェイスを洗練させるためにとても有効な手段だった。これはコーディング行為そのものにおける最適化となる。次はコーディングされたプログラムが実際に動作したらどういった影響が出るのかについて深く考察してみる必要がある。つまり、 パフォーマンスの最適化だ。それはプログラム動作の安定性を保証するためには欠かせない技術といえる。

よいプログラムとは、これらのどちらも欠けていないと思えるものだろう。日々精進…。

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?