Pimplイディオム
CoddyのC++ジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 78/104。
Pimplイディオム(Pointer to Implementation)は、クラスの実装詳細を、前方宣言された別のクラスに移動させることで隠蔽する手法です。これにより、コンパイルの依存関係が削減され、プライベートメンバをヘッダーファイルから完全に隠すことができます。
核心となる考え方は単純です。クラス内でプライベートメンバを直接宣言する代わりに、ソースファイル内でのみ定義される実装クラスへのポインタを宣言します:
// Widget.h
#include <memory>
class Widget {
public:
Widget();
~Widget();
void doSomething();
private:
class Impl; // 前方宣言
std::unique_ptr<Impl> pImpl;
};// Widget.cpp
#include "Widget.h"
#include <iostream>
class Widget::Impl {
public:
int data = 42;
void process() { std::cout << "Processing: " << data << "\n"; }
};
Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;
void Widget::doSomething() { pImpl->process(); }主な利点は、コンパイルのファイアウォールとバイナリ互換性です。Implクラスを変更した際、ソースファイルのみを再コンパイルすればよく、ヘッダーをインクルードしているすべてのファイルを再コンパイルする必要はありません。これにより、大規模なプロジェクトでのビルド時間が劇的に短縮されます。
注意点として、unique_ptr が Impl を削除するためにその完全な型を必要とするため、デストラクタは(たとえデフォルト実装であっても)ソースファイル内で定義される必要があります。これは、Pimplを初めて使用する際によくある落とし穴です。
チャレンジ
簡単Pimplイディオムを使用して、ヘッダーファイルから暗号化の実装の詳細を隠す、安全なメッセージハンドラーを構築しましょう。これは、Pimplがいかにコンパイルのファイアウォールを作成するかを示しています。あなたのヘッダーをインクルードする人は、メッセージ処理の内部動作を見ることができません。
コードを3つのファイルに分けて構成します:
SecureMessage.h:SecureMessageクラスのパブリックインターフェースを定義します。クラスには、前方宣言された
Implクラスと、それへのstd::unique_ptrが必要です。パブリックインターフェースには以下を含める必要があります:- 元のメッセージを受け取る
const std::string&を引数に取るコンストラクタ - デストラクタ(ここで宣言し、.cppファイルで定義する必要があります)
- 暗号化キーを設定するための
setKey(int key)メソッド - 暗号化されたメッセージを
std::stringとして返すgetEncrypted()メソッド - 元のメッセージを返す
getOriginal()メソッド
ヘッダーにはパブリックインターフェースのみを表示し、暗号化がどのように機能するかという実装の詳細はここでは見えないようにする必要があります。
- 元のメッセージを受け取る
SecureMessage.cpp: ネストされたImplクラスを定義し、すべてのメソッドを実装します。Implクラスは、元のメッセージ、暗号化キー(デフォルトは0)を保持し、実際の暗号化ロジックを処理する必要があります。暗号化には単純なシーザー暗号を使用します。各文字をキーの値だけシフトさせます。例えば、キーが3の場合、'a'は'd'になり、'z'は一周して'c'になります。暗号化は小文字(a-z)のみに適用し、他のすべての文字は変更しないでください。
unique_ptrは完全なImpl型を必要とするため、デストラクタをここで定義(デフォルトでも可)することを忘れないでください。main.cpp: 2つの入力を読み取ります:- メッセージ文字列(スペースを含む場合があります)
- 暗号化キー(整数)
SecureMessageオブジェクトを作成し、キーを設定して、結果を表示します:Original:に続いて元のメッセージを出力しますEncrypted:に続いて暗号化されたメッセージを出力します
例えば、入力がhello worldと3の場合:
Original: hello world
Encrypted: khoor zruog入力がxyz abcと5の場合:
Original: xyz abc
Encrypted: cde fghヘッダーファイルがシーザー暗号の実装について何も明らかにしていないことに注目してください。これがPimplの力です。後で別の暗号化アルゴリズムに変更した場合、ヘッダーをインクルードしているファイルではなく、SecureMessage.cppだけを再コンパイルすれば済みます。
チートシート
Pimplイディオム(Pointer to Implementation:実装へのポインタ)は、実装の詳細をソースファイル内でのみ定義される別のクラスに移動させることで隠蔽し、コンパイル・ファイアウォールを構築します。
ヘッダー内での前方宣言を含む基本構造:
// Widget.h
#include <memory>
class Widget {
public:
Widget();
~Widget(); // ヘッダーで宣言し、.cppで定義する必要がある
void doSomething();
private:
class Impl; // 前方宣言
std::unique_ptr<Impl> pImpl;
};ソースファイル内での実装:
// Widget.cpp
#include "Widget.h"
class Widget::Impl {
public:
int data = 42;
void process() { /* 実装 */ }
};
Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default; // .cppで定義する必要がある
void Widget::doSomething() { pImpl->process(); }主なポイント:
unique_ptrがImplを削除するために完全な型を必要とするため、デストラクタは(デフォルト指定する場合でも)ソースファイル内で定義する必要があります。Implへの変更はソースファイルの再コンパイルのみを必要とし、ヘッダーをインクルードしているファイルの再コンパイルは不要です。- コンパイル・ファイアウォールとバイナリ互換性を提供します。
自分で試してみよう
#include <iostream>
#include <string>
#include "SecureMessage.h"
int main() {
// メッセージを読み込む(スペースが含まれる場合がある)
std::string message;
std::getline(std::cin, message);
// 暗号化キーを読み込む
int key;
std::cin >> key;
// TODO: メッセージを使用して SecureMessage オブジェクトを作成する
// TODO: 暗号化キーを設定する
// TODO: "Original: " に続いて元のメッセージを出力する
// TODO: "Encrypted: " に続いて暗号化されたメッセージを出力する
return 0;
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
3コンストラクタとデストラクタ
デフォルトコンストラクタ引数付きコンストラクタコピーコンストラクタムーブコンストラクタコンストラクタ初期化リスト委譲コンストラクタデストラクタ詳解Rule of Three / Five / Zeroまとめ - Stringクラス6演算子オーバーロード
演算子オーバーロード入門算術演算子のオーバーロード比較演算子のオーバーロードストリーム演算子代入演算子のオーバーロード[] と () 演算子のオーバーロード型変換演算子まとめ:Matrix クラス