Modern C++ Design 読書ノート 2/3¶
- 著者:
Andrey Alexandrescu
- 訳者:
村上雅章
- 出版社:
ピアソン・エデュケーション
- 発行年:
2001 年
- ISBN:
978-4-89471-435-9
第 5 章 汎用のファンクタ¶
「値のセマンティックスを伴ったオブジェクト」という言い回しが頻出する。これはオブジェクトがコピー、代入、値による引渡しが完全にサポートされていることを意味する。
<関数へのポインタをかなり現代風にアレンジしたもの> (p. 105)
<状態を保存でき、メンバ関数を起動できる> (p. 105) ので、単なる関数ポインタより有利。
Command デザインパターンの解説セクション。
<作業がどのようにして行われるのかを起動側は気にしなくても良い> (p. 106)
<呼び出しを無期限に延期できる> (p. 107)
<Command パターンでは、処理を実行するために必要な環境を整えるタイミングが、処理を実行するタイミングと異なっています> (p. 107)
<Command パターンの実装を手作業で行う場合、スケーラビリティに劣ることがあります。つまり、小さな機能を持った具体的な Command クラスを数多く記述しなければならず(それぞれが、
CmdAddUser
,CmdDeleteUser
,CmdModifyUser
といったアプリケーション中の単一動作を表します)、それぞれに特定オブジェクトに対する特定のメンバ関数を単に呼び出すだけのExecute
メンバ関数を保持させなければならないのです> (p. 108): 心当たりありまくり。
<Command パターンにおける
Command::Execute
は、C++ におけるユーザ定義演算子operator()
となるべきです> (p. 111): 呼び出しシンタックスをoperator()
に統一することで、Functor
が他のFunctor
を保持できるようになるから。クラステンプレート
Functor
を、戻り値の型がResultType
で、パラメータリストをタイプリストTList
で表現する。Functor
の定義は一回。Pimpl パターンにより実装を別のクラステンプレートFunctorImpl
で行う。FunctorImpl
を部分特殊化をしまくって、引数リストのパラメータ数が十分大きくても対応できるようにしておく。// p. 114 より引用 template <typename R, class TList> class FunctorImpl; template <typename R> class FunctorImpl<R, NullType> { public: virtual R operator()() = 0; virtual FunctorImpl* Clone() const = 0; virtual ~FunctorImpl(){} }; template <typename R, typename P1> class FunctorImpl<R, TYPELIST_1(P1)> { public: virtual R operator()(P1) = 0; virtual FunctorImpl* Clone() const = 0; virtual ~FunctorImpl(){} }; ...
Functor::operator()
は FunctorImpl::operator()
へ転送を行う必要がある。
<
Functor
の定義内に任意のパラメータ数で全てのoperator()
を定義することができる> (p. 115)第 3 章で紹介した
TypeAtNonStrict<...>::Result
を十分な個数だけtypedef
する。TypeAtNonStrict
はTypeAt
のゆるゆるバージョン。力作業で
operator()
のオーバーロードをその個数分だけ実装する。
Functor
オブジェクトの構築に関する考察。
FunctorImpl
のサブクラスFunctorHandler
を定義して、Functor
のコンストラクターで Pimpl メンバーデータにセットする。
「メンバ関数へのポインタ」に関する考察。
C++ では全てのオブジェクトには型があるが、
operator.*
とoperator->*
の結果は何か違うものだ。
バインダーに関する考察。ちょっと読みにくいのと、BinderFirst
しか議論していないのが惜しい。任意の位置のパラメータにバインドするバインダーの話を振らないと。
この章の残りの話題は、
Command パターンの話をしていたので、マクロやらアンドゥ・リドゥの話。
「参照の参照」問題回避のため、traits を
Functor::operator()
にクッションする。// 例えば型 Parm1 が組み込み型でない場合、 // p1 の型は Parm1& となる。 // const が付いていたら const Parm1& となる。 R operator()( typename TypeTraits<Parm1>::ParameterType p1, typename TypeTraits<Parm2>::ParameterType p2) { return (*spImpl_)(p1, p2); }
<典型的な 32 ビットのシステムの場合、(略)メンバ関数へのポインタは 16 バイト> (p. 132) となる。
など。
第 6 章 Singleton の実装¶
<Singleton デザイン・パターンの実装で「正解」というものは存在しません。(略)扱っている問題次第で最適なものとなるのです> (p. 137)
<
static
データ +static
関数 != Singleton> (p. 138)<
static
な関数はvirtual
にできない> (p. 138)<Singleton の実装では、2 番目のインスタンスを生成しないようにしながら、オブジェクトの生成と唯一性の管理に集中することになる> (p. 139)
デフォルト・コンストラクター、コピー・コンストラクター、代入演算子は
private
に宣言することは承知しているが、これを読むまでデストラクターもprivate
にするのを忘れていた。
基本を説明してすぐに Singleton オブジェクトの破棄に関する議論が始まる。これ以降の議論は、デザインパターンの本ではまずお目にかかったことのないものだ。
<リソース・リークを避ける唯一の正しい手段とは、アプリケーションの終了時に Singleton オブジェクトを削除することです。問題は、その破棄後に、該当 Singleton に対するアクセスが発生しないようなタイミングを注意深く設定しなければならない点です> (p. 142)
次のタイプの実装を Meyers の Singleton と呼ぶことにする。
Singleton& Singleton::Instance() { static Singleton obj; return obj; }
<Meyers の Singleton は、アプリケーションの終了処理における最も簡単な Singleton の破棄手段を提供しています> (p. 143)
死んだ参照の議論。Keyboard
, Display
, Log
という 3 クラスがそれぞれ
Singleton な場合で、Keyboard
と Display
のエラーが Log
に報告するような状況を考察する。この問題を KDL 問題と呼ぶことにする。
<この 3 つの Singleton を Meyers の Singleton で実装した場合、プログラムは正しく動作しないのです> (p. 144)
<妥当な方法は、Singleton に死んだ参照の検出をさせることです> (p. 144):
Singleton::Instance
で検出させることで、何らかのエラーハンドリングをする。
Phoenix Singleton なる概念を導入する。デストラクトされたオブジェクトのあったメモリに、再度オブジェクトをコンストラクトするというものだ。
Singleton::OnDeadReference
で placement new の機能を利用し、pInstance_
にSingleton
オブジェクトを構築する。atexit
に破棄関数KillPhoenixSingleton
を登録する。KillPhoenixSingleton
ではpInstance_
に対して明示的にデストラクターを呼び出す。<new
を使用すると(略)コンパイラによる自動破棄が行われなくなるためです> (p. 147)ちなみに
atexit
にはキズがある。<規格では、atexit
を用いた関数の登録中に他のatexit
による登録が発生した場合の定義が行われていない> (p. 147)
次の議論は、「Singleton に寿命レベルを導入する」というもの。
前節の戦略だと、状態を保持するような Singleton では復活し切れないことは明白。
KDL 問題は「K, D よりも L のほうが長生きである」ことが表現できれば問題解決だ。
<ここで出てくる寿命の制御というコンセプトは、Singleton のコンセプトとは独立したものです。オブジェクトの寿命が長いほど、破棄が後にまわされるのです> (p. 149)
SetLongevity
関数の「仕様」は p. 151 のリスト参照。atexit
の呼び出しを含むのがポイント。
Warning
寿命を指定する Singleton の実装法について数ページにわたる説明があるが、読むのが面倒になったのでスキップ。
マルチスレッド対応。
<共有されるグローバル・リソースというものは全て、競合条件とスレッドに関連する問題の元凶となり得るのです> (p. 155)
今では有名になった手法だが、Doug Schdmit と Tim Harrison が発案した (1996) Double-Checked Locking パターンを紹介している。
// p. 157 より引用 Singleton& Singleton::Instance() { if(!pInstance_) { Guard myGuard(lock_); if(!pInstance_) { pInstance_ = new Singleton; } } return *pInstance_; }
ただし、ある種のマルチプロセッサではこのパターンが使えない。使えるか否かを決定するには <コンパイラのドキュメントを熟読しなければならない> (p. 157)
<少なくとも、
pInstance_
の次にvolatile
修飾子を置くことです> (p. 157)
これまでの分析を総合する。
SingletonHolder
を 3 つのポリシーに分解する。Creator
:pInstance_
の初期化ポリシー。Lifetime
: 「通常」「復活アリ」「寿命制御」「無限」の 4 パターンを提供している。Threading
: シングルスレッド or マルチスレッド。
// p. 160 より引用 template < class T, template <class> class CreationPolicy = CreateUsingNew, template <class> class LifetimePolicy = DefaultLifetime, template <class> class ThreadingModel = SingleThreaded > class SingletonHolder;
<インスタンスの型は
T*
ではなくThreadingModel<T>::VolatileType*
です> (p. 160): マルチスレッド環境では仇になる、コンパイラによるある種の最適化処理を抑止するため。<
SingletonHolder
がDestroySingleton
を呼び出すことはありません> (p. 161)KDL 問題の解として、仮コードを p. 164 に掲載している。
第 7 章 スマート・ポインタ¶
<スマート・ポインタとは、operator->
と単項演算子 operator*
を実装することによって単純なポインタとして使えるようにした C++ のオブジェクトです。スマート・ポインタは、れっきとしたポインタのシンタックスとセマンティックスに加えて、メモリ管理やロックといった処理を内部で実行することによって、指しているオブジェクトの寿命を注意深く管理するという面倒な作業からアプリケーションを解放します>
(p. 167) と、スマートポインタの定義を簡潔に与えている。
<スマート・ポインタとは、シンタックスとある種のセマンティックスに関して、通常のポインタを模倣するような C++ のクラスです> (p. 167)
<既存の高品質なスマート・ポインタでは、たいていの場合、以下のコードのようにポインタの型によってテンプレート化されています> (p. 168)
template <class T> class SmartPtr { ... private: T* pointee_; };
<値のセマンティックスが存在するオブジェクトとは、コピーや代入が可能なオブジェクトのことです> (p. 169)
ほとんどのスマートポインタには <所有権管理機能> (p. 169) が提供されている。
<
pointee_
の型は必ずT*
なのでしょうか> (p. 170)operator->
のメカニズムを応用した <事前および事後の関数呼び出し> (p. 170)ハンドルとポインタの類似性。 <セマンティックスと管理方法を見た場合、ハンドルはポインタと良く似たものなのです> (p. 171)
<メンバ関数というものはスマート・ポインタに適したものではないのです> (p. 172)
なぜかというと、
T
の解放のためのメンバ関数呼び出しと、SmartPtr<T>
のそれが似ていて紛らわしいから。SmartPtr<Printer> spRes = ... ... spRes->Release(); spRes.Release();
<スマート・ポインタは、メンバ関数を使ってはいけないのです。つまり、
SmartPtr
は非メンバ関数のみを使用するわけです> (p. 172)
所有権に関する考察。
ディープ・コピー方式
ポリモフィズムをサポートするため、コピーには
T
のコピーコンストラクタは利用できない。別途コピー用のポリシーを設けて、SmartPtr
のテンプレート引数とする。
COW 方式
COW 方式とは、「指しているオブジェクトが最初に更新されるタイミングでコピーする」というもの。
スマート・ポインタではその「タイミング」が把握できそうにないので、不採用とする。
参照カウント方式
<同じオブジェクトを指しているスマート・ポインタの総数を追跡する> (p. 176) 方式。
その総数カウンターを被参照オブジェクトにくっつける方式を侵入型参照カウント方式 (p. 177) と呼ぶ。
参照リンク方式
同じオブジェクトを指す
SmartPtr
が双方向リンクリスト構造をなす。リストが空になるタイミングが、被参照オブジェクトの破棄タイミングとなる。
破壊型コピー方式
SmartPtr
のコピーを行った場合、コピー元のオブジェクトが破壊される方式。std::auto_ptr
はまさにこれ。<破壊型コピー方式を用いたスマート・ポインタは、値のセマンティックスをサポートしていないため、コンテナに格納することができず、たいていの場合、生のポインタを扱うのと同じくらい細心の注意を払って扱わなければならないのです> (p. 180)
<単項演算子 opearator&
のオーバーロードはお勧めできない> (p. 181)
生のポインタ型への暗黙の変換はサポートしない。かわりに明示的な変換のための関数を提供する。
SmartPtr<T>
とT*
の等価性テストのため、考え得る全ての組み合わせのoperator==
およびoperator!=
を提供する。なおかつ、
SmartPtr<T>
とU*
のテストのために、テンプレート版 (p. 187) も追加する。算術型への変換は
operator bool
と同じ理由で推奨できない (p. 188)
Warning
順序比較のセクションは、読むのが面倒になったのでスキップ。
<スマート・ポインタにおけるチェックの問題は、初期化時と参照外し時という 2 つのカテゴリに分類できます> (p. 192)
<経験則としては、ポインタのチェックを厳格に行うことから始めて、プロファイラ結果に応じて、チェックの除去が可能なスマート・ポインタを選ぶというのが良いでしょう> (p. 193)
<エラーを報告するための最も優れた方法は、例外をスローすることです> (p. 193)
マルチスレッド問題は付録 A まで取っておくか。
ここ (p. 196) で紹介されている
LockingProxy
でのoperator->
トリックはおさえること。マルチスレッド参照カウント方式と、マルチスレッド参照リンク方式。
クラスレベルのロックと、オブジェクトレベルのロック。
これまでの分析を総合する。
<私たちは問題をポリシーと呼ぶ小さなクラスへと分割するのです。そして、各ポリシー・クラスでは、たった 1 つの問題を取り扱います> (p. 200)
<
SmartPtr
の宣言中に現れるポリシーの順序は、最も良くカスタマイズされるものが先頭に来るようになっています> (p. 201)template < typename T, template <class> class OwnershipPolicy = RefCounted, class ConversionPolicy = DisallowConversion, template <class> class CheckingPolicy = AssertCheck, template <class> class StoragePolicy = DefaultSPStorage > class SmartPtr;
第 8 章 オブジェクト・ファクトリ¶
この章のテーマは Factory Method デザインパターン。個人的には C++ では最もコードが書きにくいパターンだと思っているので、楽しく読めた。
<ここで問題になるのは、実際に導出を行う
Derived
という型名をnew
演算子の起動時に記述しなければならない点です。(略)ある意味では、使用してはいけないとされているコード中の数値定数と良く似ています> (p. 209)<型は必ずコンパイル時点で既知のものでなければならないのです> (p. 209)
<C++ におけるオブジェクトの生成では、呼び出し側と導出された具体的なクラスを束縛することになるのです> (p. 210)
例えば、ライブラリーがユーザー定義クラスのオブジェクト生成を行う必要がある場合、これはオブジェクト・ファクトリが必要とされるケースのひとつだ。
説明コードの
DocumentManager::NewDocument
ではnew
演算子ではなく、CreateDocument
仮想メソッド呼び出しで(ユーザー定義型の)オブジェクトを生成する。この
CreateDocument
のような役割を持つメソッドを Factory Method と呼ぶ。
保存ファイルからオブジェクトを再現する際にも、オブジェクト・ファクトリが必要だ。
<何故言語自身にオブジェクトを生成するための柔軟な手段が備わっていないのでしょうか?> (p. 212)
<C++ でオブジェクト・ファクトリを作り出すのは難しい問題になる> (p. 213)
Drawing::Load
コード (pp. 214-215) は現場でよく見かけるパターン。いい題材だ。<唯一の問題は、オブジェクト指向にける最も重要な規則に反している点です> (p. 215)
型の「タグ」に基づいて
switch
文を書いている点。Shape
の派生クラスへの言及が、単独のソースファイルに集積される点。拡張することが難しい。
クラス
ShapeFactory
でShape
派生オブジェクトを生成する関数のポインタのマップを管理する。各
Shape
派生クラスの実装ファイルで、p. 217 のコードを機械的に記述すればよい。<このコードは、
std::map
クラス・テンプレートに馴染みのない方にとっては、若干説明が必要かもしれません> (p. 218) とあるが、そんな方はこの本の読者なんかやってないと思う。
マップのキーを整数型ではなく、もっとそれらしいものにしようではないかという議論。
<こういったことから帰結できる唯一の結論は、型識別子の管理はオブジェクト・ファクトリ自身の管轄ではないということです。 C++ という言語が、ユニークで永続的な型 ID を保証していない以上、型 ID の管理はプログラマが対処しなければならない問題なのです> (p. 219)
ここからオブジェクト・ファクトリの一般化について議論する。
<ファクトリは具体的な成果物を知る必要がない> (p. 220) ので、「具体的な成果物」は
Factory
のテンプレートパラメータにならない。<エラー時の取り扱いコードを
CreateObject
メンバ関数から無くし、FactoryError
ポリシーに分離しなければなりません> (p. 221)
template
<
class AbstractProduct,
class IdentifierType,
class ProductCreator = AbstractProduct* (*)(),
template <typename, class>
class FactoryErrorPolicy = DefaultFactoryError
>
class Factory;
Todo
クローン・ファクトリは読みとばす。「共変の戻り型」の用語解説はおさえておくこと。
オブジェクト・ファクトリは通常 Singleton であることが自然。
// p. 228 より引用
typedef SingletonHolder< Factory<Shape, std::string> > ShapeFactory;
以前の Functor
を ProductCreator
とすることも可能。
// p. 228 より引用
typedef SingletonHolder
<
Factory<Shape, std::string, Functor<Shape*> >
>
ShapeFactory;