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を実装する例
//-----------------------------------------------------------------------------
//! @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 へのポインタ(実装隠蔽)
};
//-----------------------------------------------------------------------------
//! @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();
}
//-----------------------------------------------------------------------------
//! @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を使用する際の慣習となっている変数名です。
private:
struct Impl; // Impl の前方宣言
std::unique_ptr<Impl> impl; // Impl へのポインタ(実装隠蔽)
内部クラスの実装
cppにで、内部クラスとして、Implを実装します。
本記事ではActorクラスの内部実装ですので、Actor::Implとします。
//-----------------------------------------------------------------------------
//! @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にすることで、ヘッダから見て不完全な型でも保持することが出来ます。
//-----------------------------------------------------------------------------
//! @brief Actor のコンストラクタ
//-----------------------------------------------------------------------------
Actor::Actor(float x, float y)
//-----------------------------------------------------------------------------
// Implのインスタンスを生成して Actor に渡す
//-----------------------------------------------------------------------------
: impl(std::make_unique<Impl>(x, y)) {
}
総括
-
Pimplを使用することで、ヘッダの依存関係を減らすことが出来る -
cppへ依存を隠蔽するため、ビルド時間を高速化することが出来る -
コンストラクタを呼ぶ際に、ヒープメモリを確保する必要があるため、頻繁にインスタンス生成するクラスはランタイムに重くなるというデメリットもある