1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C++】Pimplを使って依存関係を減らそう

1
Posted at

C++でエンジンやツールなどを作成していると、ヘッダが重い依存が伝播するいじると再コンパイルが発生などといった問題が発生します。

そういった問題に対しては、Pimpl(Pointer to Implementation)という手法を使用することで、解決することが出来ます。

Pimplを使用することで

  • ヘッダから実装を完全に追い出せる
  • 依存関係が減らせる
  • ビルド時間が大幅に短縮される
  • 処理やメンバを隠蔽できる

といったメリットが得られます。

本記事では、ゲーム開発でよく用いられるActorクラスの実装を例に、Pimplの実装例を示したいと思います。

前提知識

  • C++の基礎文法を理解している
  • C++のスマートポインタについて理解している

Pimplとは

Pimpl(Pointer to Implementation)とは、実装をヘッダから追い出すためのイディオムです。

クラスの内部実装(メンバ変数、処理など)をImpl(Implementation)という別クラスに分離し、ポインタで保持することで、ヘッダを軽量化します。

メリットとデメリット

メリット

  • ヘッダに実装をかかなくて良い

    • 重い依存をcppへ閉じ込めることが出来る
    • ポインタで不完全型をメンバに持つ
  • ビルドを高速化できる

    • ヘッダへの重い依存が減るため、コンパイル時の負荷が軽減される
  • 実装の隠蔽ができる

    • 外部ライブラリの型をヘッダに含まなくてよいため、APIが綺麗に保てる
  • ABI安定性が高い

    • クラスのサイズが固定されるため、内部メンバを追加してもABIが破壊されない
    • DLL開発ではかなり重宝する
  • 保守性が高い

    • 内部構造を変更しても、ヘッダが不変であるため、リファクタリングが容易

デメリット

  • ヒープ確保が必要

    • std::unique_ptr<Impl>newするのでコンストラクタでヒープ確保が発生
    • 頻繁にインスタンスを生成するものには使いづらい
  • デバッグが少し面倒

    • 実装がImplに隠れているため、デバッガで追う際に一ステップ多くなる

サンプルコード

ActorクラスでPimplを実装する例

Actor.hpp
//-----------------------------------------------------------------------------
//! @file   Actor.hpp
//! @brief  Actorクラスの宣言
//! @author つきの
//-----------------------------------------------------------------------------
#pragma once
#include <memory>

//-----------------------------------------------------------------------------
//! @class   Actor
//! @brief   ゲーム内のアクターを表すクラス
//! @details Implを用いて実装を隠蔽し、インターフェースをシンプルに保つ
//-----------------------------------------------------------------------------
class Actor {
public:
    //-----------------------------------------------------------------------------
    //! @brief アクターを初期位置で生成する
    //! @param x [in] 初期X座標
    //! @param y [in] 初期Y座標
    //-----------------------------------------------------------------------------
    Actor(float x, float y);

    //-----------------------------------------------------------------------------
    //! @brief デストラクタ(Impl の破棄を cpp に委譲)
    //-----------------------------------------------------------------------------
    ~Actor();

    //-----------------------------------------------------------------------------
    //! @brief アクターの状態を更新する
    //! @param dt [in] 経過時間(秒)
    //-----------------------------------------------------------------------------
    void Update(float dt);

    //-----------------------------------------------------------------------------
    //! @brief アクターの状態をコンソールに描画する
    //-----------------------------------------------------------------------------
    void Draw() const;

private:
    struct Impl;                 // Impl の前方宣言
    std::unique_ptr<Impl> impl;  // Impl へのポインタ(実装隠蔽)
};
Actor.cpp
//-----------------------------------------------------------------------------
//! @file	Actor.cpp
//! @brief	Actorクラスの実装
//! @author つきの
//-----------------------------------------------------------------------------
#include "Actor.hpp"
#include <iostream>

//-----------------------------------------------------------------------------
//! @struct  Actor::Impl
//! @brief   Actorクラスの実装を隠蔽するための構造体
//! @details Actorクラスの内部でのみ使用され、Actorのインターフェースをシンプルに保つ
//-----------------------------------------------------------------------------
struct Actor::Impl {
	float x;
	float y;
	float vx = 1.0f;
	float vy = 0.5f;

	//-----------------------------------------------------------------------------
	//! @brief Impl のコンストラクタ
	//! @param px [in] 初期X座標
	//! @param py [in] 初期Y座標
	//-----------------------------------------------------------------------------
	Impl(float px, float py)
		: x(px), y(py) {
	}

	//-----------------------------------------------------------------------------
	//! @brief アクターの位置を更新する
	//! @param dt [in] 経過時間(秒)
	//-----------------------------------------------------------------------------
	void Update(float dt) {
		x += vx * dt;
		y += vy * dt;
	}

	//-----------------------------------------------------------------------------
	//! @brief アクターの状態をコンソールに出力する
	//-----------------------------------------------------------------------------
	void Draw() const {
		std::cout << "Actor Position: (" << x << ", " << y << ")\n";
	}
};

//-----------------------------------------------------------------------------
//! @brief Actor のコンストラクタ
//-----------------------------------------------------------------------------
Actor::Actor(float x, float y)
//-----------------------------------------------------------------------------
// Implのインスタンスを生成して Actor に渡す
//-----------------------------------------------------------------------------
	: impl(std::make_unique<Impl>(x, y)) {
}

//-----------------------------------------------------------------------------
//! @brief デストラクタ(Implの破棄は unique_ptr に任せる)
//-----------------------------------------------------------------------------
Actor::~Actor() = default;

//-----------------------------------------------------------------------------
//! @brief ImplのUpdateを呼び出す
//-----------------------------------------------------------------------------
void Actor::Update(float dt) {
	impl->Update(dt);
}

//-----------------------------------------------------------------------------
//! @brief ImplのDrawを呼び出す
//-----------------------------------------------------------------------------
void Actor::Draw() const {
	impl->Draw();
}
main.cpp
//-----------------------------------------------------------------------------
//! @file	main.cpp
//! @brief	エントリポイント
//! @author つきの
//-----------------------------------------------------------------------------
#include "Actor.hpp"
#include <chrono>
#include <thread>

// エントリポイント
int main() {
	// Actor クラスを生成
	Actor player(0.0f, 0.0f);

	//-----------------------------------------------------------------------------
	// 位置更新デモ(5ループ分)
	//-----------------------------------------------------------------------------
	for (int i = 0; i < 5; ++i) {
		player.Update(1.0f);   // 更新
		player.Draw();		   // 描画を模したコンソール出力
		std::this_thread::sleep_for(std::chrono::milliseconds(500));
	}
	// プログラムの終了
	return 0;
}

クラス図

前方宣言とポインタ

privateで前方宣言とポインタを宣言します。

Implという変数名は、Implementation(実装)の略語で、Pimplを使用する際の慣習となっている変数名です。

Actor.hpp
private:
    struct Impl;                 // Impl の前方宣言
    std::unique_ptr<Impl> impl;  // Impl へのポインタ(実装隠蔽)

内部クラスの実装

cppにで、内部クラスとして、Implを実装します。
本記事ではActorクラスの内部実装ですので、Actor::Implとします。

Actor.cpp
//-----------------------------------------------------------------------------
//! @struct  Actor::Impl
//! @brief   Actorクラスの実装を隠蔽するための構造体
//! @details Actorクラスの内部でのみ使用され、Actorのインターフェースをシンプルに保つ
//-----------------------------------------------------------------------------
struct Actor::Impl {
	float x;
	float y;
	float vx = 1.0f;
	float vy = 0.5f;

	//-----------------------------------------------------------------------------
	//! @brief Impl のコンストラクタ
	//! @param px [in] 初期X座標
	//! @param py [in] 初期Y座標
	//-----------------------------------------------------------------------------
	Impl(float px, float py)
		: x(px), y(py) {
	}

	//-----------------------------------------------------------------------------
	//! @brief アクターの位置を更新する
	//! @param dt [in] 経過時間(秒)
	//-----------------------------------------------------------------------------
	void Update(float dt) {
		x += vx * dt;
		y += vy * dt;
	}

	//-----------------------------------------------------------------------------
	//! @brief アクターの状態をコンソールに出力する
	//-----------------------------------------------------------------------------
	void Draw() const {
		std::cout << "Actor Position: (" << x << ", " << y << ")\n";
	}
};

コンストラクタ

コンストラクタで、内部実装を生成して渡します。
unique_ptrにすることで、ヘッダから見て不完全な型でも保持することが出来ます。

Actor.cpp
//-----------------------------------------------------------------------------
//! @brief Actor のコンストラクタ
//-----------------------------------------------------------------------------
Actor::Actor(float x, float y)
//-----------------------------------------------------------------------------
// Implのインスタンスを生成して Actor に渡す
//-----------------------------------------------------------------------------
	: impl(std::make_unique<Impl>(x, y)) {
}

総括

  • Pimplを使用することで、ヘッダの依存関係を減らすことが出来る
  • cppへ依存を隠蔽するため、ビルド時間を高速化することが出来る
  • コンストラクタを呼ぶ際に、ヒープメモリを確保する必要があるため、頻繁にインスタンス生成するクラスはランタイムに重くなるというデメリットもある
1
0
2

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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?