パソコンでプログラミングしよう ウィンドウズC++プログラミング環境の構築
1.6.3.6(15)
pimplイディオム

本サイトのプログラミングで多用するpimplイディオムを説明する。

pimplイディオムは多くの別名を持ち(チェシーキャット、コンパイラファイアウォール、オペークポインタ、ディーポインタ)、ブリッジパターンC++の可視性に関する欠陥を解決するものとして応用する。本サイトのプログラミングはpimplイディオムを多用する。

pimplイディオム

C++の可視性に関する欠陥

オブジェクト指向プログラミングの真髄はカプセル化による内部構造隠蔽にあるが、C++プログラマはprivate/protectedで隠蔽の気分を味わうもののC++はほとんど何も隠さない。private/protected/publicは可視性(visibility)でなくアクセス性(accessibility)を制御する(JTC1/SC22/WG21 N4659 14)。名前の隠蔽(Name hiding)(6.3.10)は可視性を制御できそうだがスコープに関わるもので内部構造隠蔽に利用できないし、それさえもスコープ解決演算子で見えてしまう。

class Base
{
public:
void MemFunc() {}
};
class Derived:public Base
{
private:
void MemFunc(int) {}
};
int main()
{
Derived d;
d.MemFunc(0); // プライベートメンバアクセスでエラー
d.MemFunc(); // Derived::MemFunc(int)がBase::MemFunc()を隠してエラー
d.Base::MemFunc(); // スコープ解決演算子でBase::MemFunc()が見えてエラーを回避
}

クラスXで検討する。インクルードファイル(X.h)は以下で、メンバ関数はソースコードファイル(X.cpp)で定義する。サブオブジェクト(基底クラスY、メンバ変数A a_とB* pb_)を利用しそれぞれのインクルードファイルをインクルードする。Xを利用するソースコード(クライアント)はX.hをインクルードするが、クライアントにとってクラス名とパブリックメンバ以外は雑音に過ぎない。Xにしても臓物(プライベートメンバ)を晒して悪意のクライアントにクラッキングの機会を与える。さらにクライアントに無関係なインクルードを強要してコンパイル時間が増大する。プログラムの開発進展でサブオブジェクトの相互依存が複雑化し不用意な循環参照さえ発生する場合がある。

X.h

#include "Y.h"
#include "A.h"
// #include "B.h" // X::pb_はポインタなので不完全でも良い
class X:public Y
{
private:
A a_;
B* pb_;
void PrivateMemFunc();
public:
X();
~X();
void PublicMemFunc();
}

pimplイディオムによる解決

pimplイディオムは先行宣言による不完全型とそのポインタでC++の可視性に関する欠陥を解決する。X1ImplはX1のロジック実装クラスでX1.hで先行宣言されてX1はそのポインタ(pimpl_)をプライベートメンバに保持する。クライアントがこの情報だけでクラス内部構造を窺うことはまず不可能で、かつサブオブジェクトのインクルードファイルはX1.cppへ移せる。代償としてメンバ関数に一層のリダイレクションを追加する。

X1.h

class X1Impl;
class X1
{
private:
X1Impl* pimpl_;
public:
X1();
~X1();
void PublicMemFunc();
}

X1.cpp

#include "X1.h"
#include "Y.h"
#include "A.h"
#include "B.h"
class X1Impl:public Y
{
private:
A a_;
B* pb_;
void PrivateMemFunc() {...} // PrivateMemFuncの実装
public:
X1():a_{},pb_{new B{}} {...} // コンストラクタの実装
~X1() {delete pb_;} // デストラクタの実装
void PublicMemFunc() {...} // PublicMemFuncの実装
}
X1Impl()::X1Impl():pimpl_{new X1Impl{}} {}
X1Impl()::~X1Impl() {delete pimpl_;}
void X1Impl()::PublicMemFunc() {pimpl_->PublicMemFunc();}

pimplイディオムのバリエーション

ロジック実装クラスの先行宣言には別のバリエーションがある。以下はX1Implの先行宣言とpimpl_メンバ変数定義をまとめるパターンだが先と文法等価でコーディングを1行節約する効果しか無い。どちらもX1と同じ名前空間(通常ならグローバル名前空間)に宣言するので他の名前と衝突しないように注意を払う。

class X1
{
private:
class X1Impl* pimpl_;
public:
X1();
~X1();
void PublicMemFunc();
}

以下はロジック実装クラスをX1のネストクラス(クラススコープで定義されるクラス)とする。他の名前と衝突する恐れは無く、さらに自動的にX1のフレンドとなる。本サイトのpimplイディオムはこれを採用し、ロジック実装クラス名はImplに統一してクラス外スコープではX1::Implとなる。

class X1
{
private:
class Impl;
Impl* pimpl_;
public:
X1();
~X1();
void PublicMemFunc();
}

他にpimpl_実装を生ポインタとするか標準ライブラリのスマートポインタ(std::unique_ptr、std::shared_ptr)とするかのバリエーションがあるが、これらはサンプルコードで詳細に考察する。

MY_PIMPLマクロ

X1のロジック実装クラスをネストクラス(X1::Impl)としてpimpl_をスマートポインタで実装すると、Doxygenソースコード解析はX1メンバ関数からX1::Implメンバ関数へ追跡できない。これに対応するには以下のMY_PIMPLマクロを全ソースコードがインクルードするファイル(例えばサンプルコードのwx_pch.h)に#defineし、これでpimpl_を定義する。

#define MY_PIMPL(smart_ptr,cls,name) class cls;smart_ptr<cls> name;

Doxygenソースコード解析時にはDoxyfileのPREDEFINEDオプションでMY_PIMPLを置き換えてpimpl_を生ポインタに変更する。本サイト掲示のDoxyfileは既にこのPREDEFINEDを含む。

PREDEFINED = ... \
"MY_PIMPL(smart_ptr,cls,name)=class cls;cls* name; /**<\brief smart_ptr<cls> at actual compilation, replaced with cls* when Doxygen analyzes.*/" \
...

サンプルコードのpimplイディオムはMY_PIMPLマクロを利用する。

コンストラクタ/代入演算子

X1::Implに手を加えずX1だけでpimpl_実装を変更することができる。それぞれの実装でコンストラクタ/代入演算子をどうするかは重要なテーマだが、その前にpimplイディオムに限らない一般論としてコンストラクタ/代入演算子の実装規範をクラスXで確認する。pimplイディオムの各実装についてはサンプルコードで考察する。

ルール・オブ・スリー/ファイブ/ゼロ

ルール・オブ...は特殊なメンバ関数の実装規範を示す。Xの特殊なメンバ関数とはデストラクタ~X()、コピーコンストラクタX(cv X&)、コピー代入演算子operator=(cv X&)またはoperator=(X)、ムーブコンストラクタX(cv X&&)、ムーブ代入演算子operator=(cv X&&)の五つであり、ここでcvはcv修飾子(無し、const、volatile、const volatileのいずれか)である。X(X)は許されない。ほとんどの場合に仮引数型はコピーconst X&あるいはムーブX&&でそれ以外は例外と考えて構わない。

特殊なメンバ関数を明示宣言しない場合は以下となる(N4659 15.8.1/p2、15.8.1/p5,15.8.1/p6、15.8.1/p8、15.8.2/p1、15.8.2/p2、15.8.2/p4)。

  • デストラクタはデフォルト定義で暗黙宣言される(宣言=default)。
  • コンストラクタ/代入演算子は下表のチェック(✔)全てが明示宣言されない場合にデフォルト定義で暗黙宣言される(宣言=default)。
  • コピーコンストラクタ/代入演算子が宣言=defaultとならない(✔が一つでも明示宣言される)場合は非定義で暗黙宣言される(宣言=delete)。
  • ムーブコンストラクタ/代入演算子が宣言=defaultとならない(✔が一つでも明示宣言される)場合は暗黙宣言しない(非宣言)。
宣言=default(全ての✔が明示宣言されない) デストラクタ コピーコンストラクタ コピー代入演算子 ムーブコンストラクタ ムーブ代入演算子
コピーコンストラクタ X(const X&)など ***
コピー代入演算子 operator=(const X&)など ***
ムーブコンストラクタ X(X&&)など ***
ムーブ代入演算子 operator=(X&&)など ***

宣言=defaultのコンストラクタ/代入演算子は使用される(odr-used)と暗黙定義され、それぞれサブオブジェクト(基底クラス、メンバ変数)をコピー構築/代入あるいはムーブ構築/代入する(15.8.1/p12、15.8.1/p14、15.8.2/p10、15.8.2/p12)。宣言=deleteはオーバーロード選択されるとコンパイルエラーとなるが非宣言はオーバーロード選択の対象にならない。

デストラクタを定義する場合は何らかのリソース解放をユーザーが行う場合がほとんどだが、コピーコンストラクタ/代入演算子も適切に定義しないと暗黙宣言=defaultがエラーをもたらす可能性がある。例えばコンストラクタが確保したメモリをデストラクタ本体が解放する場合、コピーコンストラクタ/代入演算子=defaultはシャローコピーしてストレイポインタ(不正な値を持つポインタ)を作り、あるいはリソースを二重解放する。実装規範はこういったエラーを避けるため、それぞれを明示宣言して意図を明確に記述させる事を目的とする。

実装規範 説明
ルール・オブ・スリー デストラクタ、コピーコンストラクタ/代入演算子の一つを明示宣言する場合、必ず三つとも明示宣言する。
ルール・オブ・ファイブ ムーブコンストラクタ/代入演算子の一つを明示宣言する場合、必ず全て(五つ)の特殊なメンバ関数を明示宣言する。
ルール・オブ・ゼロ 上二つに該当しない場合は特殊なメンバ関数の明示宣言を全て省いて構わない。

コピーはC++創世記から存在するが(Bjarne Stroustrup, The Design and Evolution of C++, Readings, Addison-Wesley, 1994; Readings, Addison-Wesley, 1998, pp.239-241)ムーブはC++11で規格に加えられ、両者の非対称性はこの歴史的背景による。C++11より前にルール・オブ・スリーは存在し、ルール・オブ・ファイブはその拡張として表現された。ルール・オブ・ファイブ(特殊なメンバ関数の一つを明示宣言する場合、必ず全てを明示宣言する)に統一すべきとの主張も多く、デフォルトコンストラクタを加えてルール・オブ・シックスにすべきとの主張もある。あくまで明示宣言に関する規範で、明示宣言して=deleteあるは=defaultとする場合を含む。

ルール・オブ・フォー(と半分)はルール・オブ・ファイブの特殊な形態で、コピー代入演算子/ムーブ代入演算子を後述コピーアンドスワップイディオムで実装する場合にoperator=(X)で共通化する。operator=(X)はコピー代入演算子なので明示宣言すると暗黙宣言のムーブ代入演算子は=deleteされ、そこでムーブ代入演算子を明示宣言して定義すると一時変数を実引数で渡す場合が曖昧でコンパイルエラーを生じるが、operator=(X)がムーブ代入演算子を兼ねて問題ない。

コピーアンドスワップイディオム

コピーアンドスワップ(およびムーブアンドスワップ)イディオムは特殊なメンバ関数の強い例外安全を保証しリソース二重解放を回避する。例外安全とは例外送出で有害な効果を生じないことで、基本的例外安全(送出前と異なるにしても有効なデータを持ち続ける)、強い例外安全(送出前のデータを維持する、コミット・オア・ロールバック)、不送出保証に分類される(Herb Sutter, Exceptional C++, Boston, Addison-Wesley, 2000; Boston, Addison-Wesley, 2002, p.38)。

クラスXの全サブオブジェクト(基底クラス、メンバ変数)のコンストラクタ/代入演算子が基本的例外安全で、かつコンストラクタX(...)が確保したリソースはデストラクタ~X()が正しく解放する事を当然の前提とする。それでも~X()の本体(デストラクタ本体)がリソース解放する場合、X(...)がリソース確保後に例外送出すると~X()がコールされず例外安全でない。例えばリソース確保自体が例外送出の可能性があるなら、そのようなリソース二つを確保して~X()本体が解放するとX(...)は例外安全でない。さらに具体例を示せば、二つのメモリポインタをnewして~X()本体でdeleteするとX(...)は既に例外安全でない。

Xの代入演算子operator=(const X&)とoperator=(X&&)のデフォルトは基本的例外安全のみを担保し、サブオブジェクトがただ一つの場合だけ強い例外安全を担保する。なぜならサブオブジェクトが複数で例えば2番目のサブオブジェクト代入が例外送出すれば1番目のみを変更して中途半端だが、一つだけなら中途半端になりようが無い。なおサブオブジェクトは基底クラスも含む事を改めて思い出しておこう。例外安全とは別に、~X()本体で解放するリソース(例えばメモリポインタ)を一つでも所有するクラスはムーブコンストラクタX(X&&)とムーブ代入演算子operator=(X&&)が仮引数に渡されたインスタンスのリソースをキャンセル(メモリポインタならヌルを代入)しないと同じリソースを二重解放してしまう。コピーアンドスワップはoperator=(const X&)/operator=(X&&)の強い例外安全を保証し、X(X&&)/operator=(X&&)のリソース二重解放を回避する。

X(const X& rhs):Y{rhs},a_{rhs.a_},pb_{rhs.pb_->clone();} {}; // B::cloneはディープコピーを行うとする
X(X&& rhs):X{} {swap(*this,rhs);}
// X(X&& rhs):X{} {*this=rhs;} // ムーブ代入演算子による表現
// X(X&& rhs):Y{std::move(rhs)},a_{std::move(rhs.a_)},pb_{std::move(rhs.pb_)} {rhs.pb_=nullptr;} // ムーブアンドスワップを使わない
X& operator=(const X& rhs) {auto tmp=rhs;swap(*this,rhs);return *this}
X& operator=(X&& rhs) {swap(*this,rhs);return *this;}
// X& operator=(X rhs) {swap(*this,rhs);return *this;} // 代入演算子の共通化
void swap(X& lhs,X& rhs)
{
using std::swap;
swap(static_cast<Y&>(lhs),static_cast<Y&>(rhs));
swap(lhs.a_,rhs.a_);
swap(lhs.pb_,rhs.pb_);
}

X(X&&)のムーブアンドスワップは不要なインスタンス構築を必要とするため、デフォルト相当のX(X&&)として仮引数インスタンスが~X()本体で開放するリソースのキャンセルを本体に追加する方が好ましい。operator=(X rhs)はoperator=(const X& rhs)とoperator=(X&& rhs)を共通化してルール・オブ・フォー(と半分)が使用する。swapオーバーロード関数はXのフレンドで全サブオブジェクトをスワップする。C++標準ライブラリはusing std::swapして非修飾(unqualified)swapをコールするため、Xのスワップは実引数依存探索(Argument Dependent Lookup, ADL)でこのオーバーロードを選択する(N4659 20.5.3.2)。オーバーロードはstd::swap関数テンプレートのXによる暗黙的特殊化と同じ処理をするが、コンストラクタや代入演算子でswapの代わりにstd::swapをコールするとstd::swapはoperator=(X&& rhs)をコールして無限回帰に陥る。

コピーアンドスワップ(およびムーブアンドスワップ)はコピーコンストラクタとswapオーバーロードを正しく書けばほとんどの場合に代入演算子が同一コードとなるため公式化している。ただしサブオブジェクトが一つで~X()本体がリソース解放しなければ、swapオーバーロードは不要で代入演算子はデフォルトで構わない事になる。

サンプルコード

pimplイディオムの優位性を簡単なクラスサンプルで比較する。このクラスは自らメモリ管理を行うワイド文字列クラスでUTF-16の格納を想定する。ワイド文字列(const wchar_t*)で構築され、コピー/ムーブのコンストラクタ/代入演算子、連結演算子(+=、+)、ワイドストリーム出力演算子(<<)、ワイド文字列取得メンバ関数(CStr)、文字長取得メンバ関数(Length)、正規表現文字列置換で新しいインスタンスを返すメンバ関数(RegexReplace)を持つ。

TMyString0はpimplイディオムを使用しない。TMyString1~5はpimplイディオムを使用して等しいロジック実装をネストクラス(Impl)に持つ。TMyString4のみ参照セマンティクスで、他はC++オブジェクトとして標準的な値セマンティクスである。TMyString0とTMyString1はデストラクタ本体でリソース解放し、swapオーバーロードを定義して代入演算子はコピーアンドスワップする。TMyString2~5はサブオブジェクト一つでデストラクタ本体でリソース解放せず、swapオーバーロードしないで代入演算子はデフォルト相当で良い。

クラス 実装方法 swapオーバーロード ルール・オブ
TMyString0 pimplを使用しない する フォー(と半分)
TMyString1 生ポインタ実装 する フォー(と半分)
TMyString2 unique_ptr実装 しなくて良い ファイブ
TMyString3 shared_ptr実装 しなくて良い ファイブ
TMyString4 shared_ptr実装で参照セマンティクス しなくて良い ゼロ
TMyString5 shared_ptr実装でコピーオンライト しなくて良い ゼロ
覚え書き
もちろんTMyString2~5もswapオーバーロードしてコピーアンドスワップ/ルール・オブ・フォー(と半分)で実装できる。ここでは生ポインタ、std::unique_ptr、std::shared_ptrの違いを際立たせるためそれぞれに異なった実装を採用した。

各クラスサンプルは以下コードで動作確認できる。コマンドプロンプトへの出力を想定する。日本語を含むソースコードで文字コードUTF-8としてCode::Blocksなら[Edit|File encoding|UTF-8]を選択し、文字コードに関するコンパイラオプションはデフォルトのままとする。出力は参照セマンティクスを除き全て等しい。

#include <iostream>
#include <memory>
#include <boost/core/demangle.hpp>
#include <boost/mpl/list.hpp>
#include <boost/mpl/for_each.hpp>
#include "TMyString0.h"
#include "TMyString1.h"
#include "TMyString2.h"
#include "TMyString3.h"
#include "TMyString4.h"
#include "TMyString5.h"
struct TMyOutput
{
template<typename T> void operator()(T) const
{
std::cout<<"***"<<boost::core::demangle(typeid(T).name())<<"***"<<std::endl;
auto str1=T{L"ライオン"};
std::wcout<<str1+L"シマウマ"<<std::endl;
std::wcout<<str1.RegexReplace(L"ライオ",L"キリ")<<std::endl;
auto str2=str1;
str1+=L"トラ";
std::wcout<<str1<<std::endl;
std::wcout<<str2<<std::endl<<std::endl;
}
};
int main()
{
std::ios_base::sync_with_stdio(false);
//std::locale::global(std::locale{""}); // Doesn't work.
std::setlocale(LC_CTYPE,"");
boost::mpl::for_each<boost::mpl::list<TMyString0,TMyString1,TMyString2,TMyString3,TMyString4,TMyString5>>(TMyOutput{});
return 0;
}

pimplを使用しない

  • 複数のサブオブジェクトを持ち、その一つをデストラクタ本体でリソース解放する。swapオーバーロードしてコピーアンドスワップ/ルール・オブ・フォー(と半分)とする。

本項目の主題と直接関係しないが、参考として処理概要をまとめる。

  • インターフェース原則(Interface Princeple)(Herb Sutter, Exceptional C++, Boston, Addison-Wesley, 2000; Boston, Addison-Wesley, 2002, pp.122-133)を満たす自由関数はフレンド関数としてTMyString0プライベートメンバへアクセスする。
  • 正規表現文字列置換は標準ライブラリ<regex>を用いる。pimplイディオムの優位性を強調するサンプルとしてインスタンスをregex_サブオブジェクトに所有(コンポジット)するが、実用ならRegexReplaceローカルで良い。
  • 正規表現文字列置換を文字列バッファ末尾へ直接挿入するイテレータとしてBackInserterを定義する。これはTMyString0のネストクラスでTMyString0プライベートメンバへアクセスする。
  • BackInserterインスタンスの内部状態がコピーで矛盾しないようにProxyネストクラスに処理を実装して参照カウントで保持する。BackInserterの*演算子はProxyインスタンスへの参照を返して代入はProxyの=演算子が行う。
覚え書き
なおmingw-w64付属の<regex>には大きなバグが残り(バージョン10.2.0確認)、長大な文字列を扱うならboost::regexの方が安心だ。

TMyString0.h

#ifndef TMYSTRING0_H
#define TMYSTRING0_H
#include <regex>
#include <iosfwd>
class TMyString0 final
{
private:
mutable std::wregex regex_;
size_t len_;
wchar_t* str_;
class BackInserter;
static wchar_t* ReallocStr(wchar_t* str,size_t sz);
static void DeallocStr(wchar_t* str);
static wchar_t* CloneStr(const wchar_t* str,size_t len);
public:
TMyString0(const wchar_t* str=0);
~TMyString0();
TMyString0(const TMyString0& rhs);
TMyString0(TMyString0&& rhs);
TMyString0& operator=(TMyString0 rhs);
friend void swap(TMyString0& lhs,TMyString0& rhs);
TMyString0& operator+=(const TMyString0& rhs);
friend TMyString0 operator+(const TMyString0& lhs,const TMyString0& rhs);
friend std::wostream& operator<<(std::wostream& lhs,const TMyString0& rhs);
const wchar_t* CStr() const;
size_t Length() const;
TMyString0 RegexReplace(const TMyString0& expr,const TMyString0& fmt) const;
};
#endif // TMYSTRING0_H

TMyString0.cpp

#include "TMyString0.h"
class TMyString0::BackInserter:std::iterator<std::output_iterator_tag,wchar_t>
{
private:
struct Proxy
{
static constexpr size_t blockSz_=256;
size_t& len_;
wchar_t*& str_;
size_t sz_;
size_t refCount_;
void Realloc(size_t sz) {str_=ReallocStr(str_,sz);sz_=sz;}
Proxy& operator=(wchar_t val)
{
while (!(str_&&len_<sz_)) {Realloc(sz_+blockSz_);}
*(str_+len_)=val;++len_;return *this;
}
Proxy(TMyString0& myStr):len_{myStr.len_},str_{myStr.str_},sz_{len_+1},refCount_{0} {}
~Proxy() {if (str_) {Realloc(len_+1);*(str_+len_)=0;}}
} *proxy_;
public:
BackInserter(TMyString0& myStr):proxy_{new Proxy{myStr}} {}
BackInserter(const BackInserter& rhs):proxy_{rhs.proxy_} {++proxy_->refCount_;}
BackInserter& operator=(BackInserter rhs) {std::swap(proxy_,rhs.proxy_);return *this;}
~BackInserter()
{
if (proxy_->refCount_) {--proxy_->refCount_;}
else {delete proxy_;}
}
Proxy& operator*() {return *proxy_;}
BackInserter& operator++() {return *this;}
BackInserter& operator++(int) {return *this;}
};
wchar_t* TMyString0::ReallocStr(wchar_t* str,size_t sz)
{
auto* newStr=static_cast<wchar_t*>(std::realloc(str,sz*sizeof(wchar_t)));
if (!newStr) {throw std::runtime_error("Memory allocation failed.");}
return newStr;
}
void TMyString0::DeallocStr(wchar_t* str) {std::free(str);}
wchar_t* TMyString0::CloneStr(const wchar_t* str,size_t len)
{return str?std::wcscpy(ReallocStr(nullptr,len+1),str):nullptr;}
TMyString0::TMyString0(const wchar_t* str):regex_{},len_{str?std::wcslen(str):0},str_{CloneStr(str,len_)} {}
TMyString0::~TMyString0() {DeallocStr(str_);}
TMyString0::TMyString0(const TMyString0& rhs):regex_{},len_{rhs.len_},str_{CloneStr(rhs.str_,len_)} {}
TMyString0::TMyString0(TMyString0&& rhs)
:regex_{std::move(rhs.regex_)},len_{std::move(rhs.len_)},str_{std::move(rhs.str_)} {rhs.str_=nullptr;}
TMyString0& TMyString0::operator=(TMyString0 rhs) {swap(*this,rhs);return *this;}
void swap(TMyString0& lhs,TMyString0& rhs)
{
using std::swap;
swap(lhs.regex_,rhs.regex_);
swap(lhs.len_,rhs.len_);
swap(lhs.str_,rhs.str_);
}
TMyString0& TMyString0::operator+=(const TMyString0& rhs)
{
if (rhs.str_)
{
bool identical=(str_==rhs.str_);
str_=ReallocStr(str_,len_+rhs.len_+1);
if (identical) {*(std::wcsncpy(str_+len_,str_,len_)+len_)=0;}
else {std::wcscpy(str_+len_,rhs.str_);}
len_+=rhs.len_;
}
return *this;
}
TMyString0 operator+(const TMyString0& lhs,const TMyString0& rhs) {auto retVal=lhs;return retVal+=rhs;}
std::wostream& operator<<(std::wostream& lhs,const TMyString0& rhs) {return lhs<<rhs.str_;}
const wchar_t* TMyString0::CStr() const {return str_;}
size_t TMyString0::Length() const {return len_;}
TMyString0 TMyString0::RegexReplace(const TMyString0& expr,const TMyString0& fmt) const
{
regex_.assign(expr.str_,std::regex_constants::extended);
auto retVal=TMyString0{};
std::regex_replace(BackInserter{retVal},str_,str_+len_,regex_,fmt.str_);
return retVal;
}

生ポインタ実装

TMyString1~5はfinal修飾して派生クラスを持たないので原則全メンバをImplに移譲するが(Herb Sutter, Exceptional C++, Boston, Addison-Wesley, 2000; Boston, Addison-Wesley, 2002, pp.109-118)、コピーコンストラクタ/代入演算子、ムーブコンストラクタ/代入演算子、swapオーバーロード、+演算子フレンド関数は例外とする。

  • TMyString0の実装を例外を除きImplへ移す。<regex>のインクルードもソースコードファイル(*.cpp)へ移す。
  • Implのクラス定義とメンバ関数/フレンド関数/ネストクラス定義は同一ファイル(*.cpp)に置く。後者の定義は全てクラスブロック内で行う。
  • Implはデストラクタ本体でリソース解放(free)するがムーブ不要でルール・オブ・スリーとする。コピーコンストラクタは定義するがコピー代入演算子は明示宣言=deleteとする。

ImplはTMyString1~5で等しく、pimplイディオムはImplを変えずにクラスの特性や挙動を変更する。

  • TMyString1~5は基底クラスを持たずプライベートメンバ変数pimpl_とパブリックメンバ関数だけを持つ。サブオブジェクトはpimpl_一つだけである。
  • pimpl_はImplの生ポインタまたはスマートポインタで、TMyString1~TMyString5のパブリックメンバ関数は例外を除きImplの同名メンバ関数をコールする。
  • TMyString1~5の+演算子フレンド関数はImplに移譲せず、+=演算子メンバ関数を用いて実装する。

TMyString1は生ポインタでpimpl_を実装する。

  • pimpl_の型をImpl*とする。
  • デストラクタ本体でpimpl_リソース解放(delete)するので、swapオーバーロードしてコピーアンドスワップ/ルール・オブ・フォー(と半分)とする。
  • コピーコンストラクタはImplコピーコンストラクタでディープコピーする。
  • swapオーバーロードはpimpl_をスワップする。代入演算子はコピーアンドスワップする。
  • ムーブコンストラクタはデフォルト相当に仮引数pimpl_へのヌル代入を追加する。

TMyString0.cppとTMyString1.cppを比較すれば、前者はネストクラス/メンバ関数/フレンド関数の記述がクラス構造を反映せず面倒だが(スコープ解決演算子をたどらなければならない、インクルードファイルを参照しなければならない、フレンド関数はさらに解りにくい)、後者はImplが直接反映して理解しやすい。パブリックメンバ関数に一層のリダイレクションを追加するのは確かに面倒だが、逆にプライベート処理の変更は好き勝手となる。インターフェース変更と実装変更の頻度を比べればpimpl_イディオムを採る方が優位と考える。

TMyString1.h

#ifndef TMYSTRING1_H
#define TMYSTRING1_H
#include <iosfwd>
class TMyString1 final
{
private:
class Impl;
Impl* pimpl_;
public:
TMyString1(const wchar_t* str=0);
~TMyString1();
TMyString1(const TMyString1& rhs);
TMyString1(TMyString1&& rhs);
TMyString1& operator=(TMyString1 rhs);
friend void swap(TMyString1& lhs,TMyString1& rhs);
TMyString1& operator+=(const TMyString1& rhs);
friend TMyString1 operator+(const TMyString1& lhs,const TMyString1& rhs);
friend std::wostream& operator<<(std::wostream& lhs,const TMyString1& rhs);
const wchar_t* CStr() const;
size_t Length() const;
TMyString1 RegexReplace(const TMyString1& expr,const TMyString1& fmt) const;
};
#endif // TMYSTRING1_H

TMyString1.cpp

#include "TMyString1.h"
#include <regex>
class TMyString1::Impl final
{
private:
mutable std::wregex regex_;
size_t len_;
wchar_t* str_;
class BackInserter:std::iterator<std::output_iterator_tag,wchar_t>
{
private:
struct Proxy
{
static constexpr size_t blockSz_=256;
size_t& len_;
wchar_t*& str_;
size_t sz_;
size_t refCount_;
void Realloc(size_t sz) {str_=ReallocStr(str_,sz);sz_=sz;}
Proxy& operator=(wchar_t val)
{
while (!(str_&&len_<sz_)) {Realloc(sz_+blockSz_);}
*(str_+len_)=val;++len_;return *this;
}
Proxy(Impl& myStr):len_{myStr.len_},str_{myStr.str_},sz_{len_+1},refCount_{0} {}
~Proxy() {if (str_) {Realloc(len_+1);*(str_+len_)=0;}}
} *proxy_;
public:
BackInserter(Impl& myStr):proxy_{new Proxy{myStr}} {}
BackInserter(const BackInserter& rhs):proxy_{rhs.proxy_} {++proxy_->refCount_;}
BackInserter& operator=(BackInserter rhs) {std::swap(proxy_,rhs.proxy_);return *this;}
~BackInserter()
{
if (proxy_->refCount_) {--proxy_->refCount_;}
else {delete proxy_;}
}
Proxy& operator*() {return *proxy_;}
BackInserter& operator++() {return *this;}
BackInserter& operator++(int) {return *this;}
};
static wchar_t* ReallocStr(wchar_t* str,size_t sz)
{
auto* newStr=static_cast<wchar_t*>(std::realloc(str,sz*sizeof(wchar_t)));
if (!newStr) {throw std::runtime_error("Memory allocation failed.");}
return newStr;
}
static void DeallocStr(wchar_t* str) {std::free(str);}
static wchar_t* CloneStr(const wchar_t* str,size_t len)
{return str?std::wcscpy(ReallocStr(nullptr,len+1),str):nullptr;}
public:
Impl(const wchar_t* str):regex_{},len_{str?std::wcslen(str):0},str_{CloneStr(str,len_)} {}
~Impl() {DeallocStr(str_);}
Impl(const Impl& rhs):regex_{},len_{rhs.len_},str_{CloneStr(rhs.str_,len_)} {}
Impl& operator=(const Impl& rhs)=delete;
Impl& operator+=(const Impl& rhs)
{
if (rhs.str_)
{
bool identical=(str_==rhs.str_);
str_=ReallocStr(str_,len_+rhs.len_+1);
if (identical) {*(std::wcsncpy(str_+len_,str_,len_)+len_)=0;}
else {std::wcscpy(str_+len_,rhs.str_);}
len_+=rhs.len_;
}
return *this;
}
friend std::wostream& operator<<(std::wostream& lhs,const Impl& rhs) {return lhs<<rhs.str_;}
const wchar_t* CStr() const {return str_;}
size_t Length() const {return len_;}
TMyString1 RegexReplace(const TMyString1& expr,const TMyString1& fmt) const
{
regex_.assign(expr.pimpl_->str_,std::regex_constants::extended);
auto retVal=TMyString1{};
std::regex_replace(BackInserter{*retVal.pimpl_},str_,str_+len_,regex_,fmt.pimpl_->str_);
return retVal;
}
};
TMyString1::TMyString1(const wchar_t* str):pimpl_{new Impl{str}} {}
TMyString1::~TMyString1() {delete pimpl_;}
TMyString1::TMyString1(const TMyString1& rhs):pimpl_{new Impl{*rhs.pimpl_}} {}
TMyString1::TMyString1(TMyString1&& rhs):pimpl_{std::move(rhs.pimpl_)} {rhs.pimpl_=nullptr;}
TMyString1& TMyString1::operator=(TMyString1 rhs) {swap(*this,rhs);return *this;}
void swap(TMyString1& lhs,TMyString1& rhs)
{
using std::swap;
swap(lhs.pimpl_,rhs.pimpl_);
}
TMyString1& TMyString1::operator+=(const TMyString1& rhs) {*pimpl_+=*rhs.pimpl_;return *this;}
TMyString1 operator+(const TMyString1& lhs,const TMyString1& rhs) {auto retVal=lhs;return retVal+=rhs;}
std::wostream& operator<<(std::wostream& lhs,const TMyString1& rhs) {return lhs<<*rhs.pimpl_;}
const wchar_t* TMyString1::CStr() const {return pimpl_->CStr();}
size_t TMyString1::Length() const {return pimpl_->Length();}
TMyString1 TMyString1::RegexReplace(const TMyString1& expr,const TMyString1& fmt) const
{return pimpl_->RegexReplace(expr,fmt);}

unique_ptr実装

std::unique_ptrは所有ポインタがただ一つであることを保証するスマートポインタである(N4659 23.11.1)。一般にスマートポインタが所有ポインタの寿命管理をするためTMyString2デストラクタ本体での解放(delete)は必要ない。しかしunique_ptrはImplが完全な位置で解体する必要があり(さもなければ不完全なポインタをdeleteするがこれは許されない)、そのunique_ptrはTMyString2デストラクタ(本体は空でも良い)が暗黙に解体するため、TMyString2デストラクタもImplが完全な位置で定義する。暗黙宣言のデストラクタはImplが不完全なクラスブロック内で定義されるため使用できず、結論として本体を空とするデストラクタをTMyString2.cppに定義する必要がある。ムーブコンストラクタ/代入演算子もデフォルトで十分だが同じ理由でTMyString2.cppに定義する。

  • pimpl_の型をunique_ptr<Impl>とする。
  • デストラクタ本体でリソース解放しないのでswapオーバーロードを省きルール・オブ・ファイブとする。
  • コピーコンストラクタはImplコピーコンストラクタでディープコピーする。
  • コピー代入演算子はImplコピーコンストラクタでディープコピーを行いpimpl_を変更する。
  • デストラクタは本体空で実装する。ムーブコンストラクタ/代入演算子はデフォルト相当で実装する。
覚え書き
本体を空とするデストラクタの必要性はpimplイディオム普及当初にホットな話題だった。当時はstd::auto_ptrだったがハーブ・サッターでさえ間違いを犯し今もリンクのExample 4(b)で確認できる。教科書では訂正されている(Herb Sutter, Exceptional C++, Boston, Addison-Wesley, 2000; Boston, Addison-Wesley, 2002, p.154)。

スマートポインタ実装の生ポインタ実装に対する優位点は例外安全であると言われる。しかしTMyString1の例外送出がストレイポインタを生み出す可能性は皆無なのは明らかだ。スマートポインタなら開発進展でpimpl_を複数持つようになったとしても例外安全を維持するが(強い例外安全を維持するにはコピーアンドスワップに戻す必要があるが)、それならImplをサブオブジェクトに分割するほうが自然だろう。それでも本サイトのpimplイディオムの大部分はunique_ptrで実装するがもはや習慣化した結果にすぎない。

TMyString2.h

#ifndef TMYSTRING2_H
#define TMYSTRING2_H
#include <memory>
#include <iosfwd>
class TMyString2 final
{
private:
class Impl;
std::unique_ptr<Impl> pimpl_;
public:
TMyString2(const wchar_t* str=0);
~TMyString2();
TMyString2(const TMyString2& rhs);
TMyString2& operator=(const TMyString2& rhs);
TMyString2(TMyString2&& rhs);
TMyString2& operator=(TMyString2&& rhs);
...
};
#endif // TMYSTRING2_H

TMyString2.cpp

#include "TMyString2.h"
#include <regex>
class TMyString2::Impl final
{
...
};
TMyString2::TMyString2(const wchar_t* str):pimpl_{new Impl{str}} {}
TMyString2::~TMyString2() {}
TMyString2::TMyString2(const TMyString2& rhs):pimpl_{new Impl{*rhs.pimpl_}} {}
TMyString2& TMyString2::operator=(const TMyString2& rhs) {pimpl_.reset(new Impl{*rhs.pimpl_});return *this;}
TMyString2::TMyString2(TMyString2&& rhs):pimpl_{std::move(rhs.pimpl_)} {}
TMyString2& TMyString2::operator=(TMyString2&& rhs) {pimpl_=std::move(rhs.pimpl_);return *this;}
...

=defaultはクラススコープ外でも使用できる(N4659 11.4.2/p4)ので以下でも良い。

...
TMyString2::TMyString2(const wchar_t* str):pimpl_{new Impl{str}} {}
TMyString2::~TMyString2()=default;
TMyString2::TMyString2(const TMyString2& rhs):pimpl_{new Impl{*rhs.pimpl_}} {}
TMyString2& TMyString2::operator=(const TMyString2& rhs) {pimpl_.reset(new Impl{*rhs.pimpl_});return *this;}
TMyString2::TMyString2(TMyString2&& rhs)=default;
TMyString2& TMyString2::operator=(TMyString2&& rhs)=default;
...

shared_ptr実装

std::shared_ptrは同一のポインタを複数が参照カウントで所有するスマートポインタである(N4659 23.11.2)。shared_ptrはunique_ptrと異なりImplが不完全な位置でも解体できるため、デストラクタなどを明示宣言=defaultにできる。

  • pimpl_の型をshared_ptr<Impl>とする。
  • swapオーバーロードを省きルール・オブ・ファイブとする。
  • コピーコンストラクタはImplコピーコンストラクタに移譲してディープコピーを行う。
  • コピー代入演算子はImplコピーコンストラクタでディープコピーを行いpimpl_を変更する。
  • デストラクタとムーブコンストラクタ/代入演算子を明示宣言=defaultとする。

unique_ptrに対する優位点はコーディングを3行削減するだけで逆に不要な参照カウントを持つ事になり、普通はshared_ptr実装を使わない。

覚え書き
unique_ptrとshared_ptrの挙動の違いは例えば以下のコードで説明できる。smart_ptr_2はコンストラクタでdeleter_メンバ変数にptr_をdeleteするdo_delete関数テンプレートのUによる暗黙的特殊化のポインタを代入し、デストラクタはdeleter_をコールする。do_deleteはコンストラクタによりTが完全な位置で実体化(Point Of Instantiation、POI)(David Vandevoorde et al., C++ Templates, Boston, Addison-Wesley, 2003, p.146)するため(N4659 17.7.1/p3)、Tが不完全な位置のデストラクタでも安全にptr_をdeleteする。
template<typename T> class smart_ptr_1 // Undestructable where T is incomplete.
{
private:
T* ptr_;
public:
smart_ptr_1(T* ptr):ptr_{ptr} {}
~smart_ptr_1() {delete ptr_;}
T* operator->() {return ptr_;}
};
template<typename T> class smart_ptr_2 // Destructable even where T is incomplete.
{
private:
T* ptr_;
void (*deleter_)(void*);
template<typename U> static void do_delete(void* ptr) {delete static_cast<U*>(ptr);}
public:
template<typename U> smart_ptr_2(U* ptr):ptr_{ptr},deleter_{do_delete<U>} {}
~smart_ptr_2() {deleter_(ptr_);}
T* operator->() {return ptr_;}
};

TMyString3.h

#ifndef TMYSTRING3_H
#define TMYSTRING3_H
#include <memory>
#include <iosfwd>
class TMyString3 final
{
private:
class Impl;
std::shared_ptr<Impl> pimpl_;
public:
TMyString3(const wchar_t* str=0);
~TMyString3()=default;
TMyString3(const TMyString3& rhs);
TMyString3& operator=(const TMyString3& rhs);
TMyString3(TMyString3&& rhs)=default;
TMyString3& operator=(TMyString3&& rhs)=default;
...
};
#endif // TMYSTRING3_H

TMyString3.cpp

#include "TMyString3.h"
#include <regex>
class TMyString3::Impl final
{
...
};
TMyString3::TMyString3(const wchar_t* str):pimpl_{new Impl{str}} {}
TMyString3::TMyString3(const TMyString3& rhs):pimpl_{new Impl{*rhs.pimpl_}} {}
TMyString3& TMyString3::operator=(const TMyString3& rhs) {pimpl_.reset(new Impl{*rhs.pimpl_});return *this;}
...

shared_ptr実装で参照セマンティクス

pimplイディオムのstd::shared_ptr実装を応用すれば"参照セマンティクス"クラスを簡単に作成できる。インスタンスは他のインスタンスと*pimpl_(Implインスタンス)を共有し、例えばPythonオブジェクトと同じ振る舞いをする。

  • pimpl_の型をshared_ptr<Impl>とする。
  • シャローコピーなのでコピーコンストラクタ/代入演算子=defaultで良く、ルール・オブ・ゼロとする。
  • +演算子フレンド関数はディープコピーしてユニークなImplインスタンスを持つインスタンスを返す。

TMyString4.h

#ifndef TMYSTRING4_H
#define TMYSTRING4_H
#include <memory>
#include <iosfwd>
class TMyString4 final
{
private:
class Impl;
std::shared_ptr<Impl> pimpl_;
public:
TMyString4(const wchar_t* str=0);
...
};
#endif // TMYSTRING4_H

TMyString4.cpp

#include "TMyString4.h"
#include <regex>
class TMyString4::Impl final
{
...
};
TMyString4::TMyString4(const wchar_t* str):pimpl_{new Impl{str}} {}
TMyString4& TMyString4::operator+=(const TMyString4& rhs) {*pimpl_+=*rhs.pimpl_;return *this;}
TMyString4 operator+(const TMyString4& lhs,const TMyString4& rhs)
{
auto retVal=TMyString4{};
retVal.pimpl_.reset(new TMyString4::Impl{*lhs.pimpl_});
return retVal+=rhs;
}
...

shared_ptr実装でコピーオンライト

pimplイディオムのstd::shared_ptr実装を応用すればコピーオンライト(COW)クラスを簡単に作成できる。インスタンスは他のインスタンスと*pimpl_(Implインスタンス)を共有し、上書きでユニークな*pimpl_へコピーして値セマンティクスとして振る舞う。

  • pimpl_の型をshared_ptr<Impl>とする。
  • シャローコピーなのでコピーコンストラクタ/代入演算子=defaultで良く、ルール・オブ・ゼロとする。
  • +=演算子メンバ関数はディープコピーしてユニークなImplインスタンスに持ち変える。
覚え書き
TMyString4とTMyString5をスレッド安全とするにはpimpl_アクセスに排他制御を必要とする。特にCOWはメモリ消費量の削減と動作高速化をメリットとして多くのライブラリが多用してきたが、現実世界では排他制御のコストが大きく今や逆効果とさえ見なされる(Herb Sutter, More Exceptional C++, Boston, Addison-Wesley, 2000; Boston, Addison-Wesley, 2002, pp.247-261)。

TMyString5.h

#ifndef TMYSTRING5_H
#define TMYSTRING5_H
#include <memory>
#include <iosfwd>
class TMyString5 final
{
private:
class Impl;
std::shared_ptr<Impl> pimpl_;
public:
TMyString5(const wchar_t* str=0);
...
};
#endif // TMYSTRING5_H

TMyString5.cpp

#include "TMyString5.h"
#include <regex>
class TMyString5::Impl final
{
...
};
TMyString5::TMyString5(const wchar_t* str):pimpl_{new Impl{str}} {}
TMyString5& TMyString5::operator+=(const TMyString5& rhs)
{
pimpl_.reset(new Impl{*pimpl_});
*pimpl_+=*rhs.pimpl_;return *this;
}
TMyString5 operator+(const TMyString5& lhs,const TMyString5& rhs) {auto retVal=lhs;return retVal+=rhs;}
...