2. "Implementing QuantLib"の和訳
Appendix A Odds and Ends 周辺の話題
A.7 Design Pattern デザインパターン
QuantLibライブラリーの中では、様々なデザインパターンが使われています。デザインパターンの詳細については、有名な Gang of Four(“4人組”) の本 (E. Gamma, R. Helm, R. Johnson, J. Vlissides, “Design Patterns”Element of Reusable Object-Oriented Software. Addison-Wesley, 1995) を参考にして下さい。ではなぜ、ここで私自身がデザインパターンについて書く必要があるのでしょうか? かつてG.K.Chestertonが記していた次の言葉があります。
Poets have been mysteriously silent on the subject of cheese
(詩人は、“チーズ”については謎のごとく題材にしないものだ)
そして、有名な4人組も、デザインパターンを実装する際に直面する現実的な問題については、何も述べていません(但し、それは彼らの問題では無い点注意して下さい)。デザインパターンの応用例はほとんど無限にありますが、彼らは4人しかいないのです。
そこで、Appendixの最後のセクションを使って、QuantLibライブラリーの要件を満たす為、我々が使ったデザインパターンの実装方法を説明したいと思います。
A.7.1 Observer Pattern オブザーバーパターン
QuantLibライブラリーでは、至る所で Observerパターンが使われています。既に、Chapter IIとIIIで、Instruments(金融商品)や TermStructure(期間構造)クラスが、このパターンを使って市場データの変動を常にウォッチし、必要な時に価格の再計算を行っているのを見てきました。
我々の実装方法は(概要を次の Listing A-27 に示します) 4人組の本で説明された方法とほぼ同じです。しかし、先ほど述べた通り、この本では教えてくれていない、いくつかの疑問点や問題点があります。
Listing A-27: ObserverとObservableクラスの概要
class Observable {
friend class Observer;
public:
void notifyObservers() {
for (iterator i=observers_.begin();
i!=observers_.end(); ++i) {
try {
(*i)->update();
} catch (std::exception& e) {
// store information for later
}
}
}
private:
void registerObserver(Observer* o) {
observers_.insert(o);
}
void unregisterObserver(Observer*);
list<Observer*> observers_;
};
class Observer {
public:
virtual ~Observer() {
for (iterator i=observables_.begin();
i!=observables_.end(); ++i)
(*i)->unregisterObserver(this);
}
void registerWith(const shared_ptr<Observable>& o) {
o->registerObserver(this);
observables_.insert(o);
}
void unregisterWith(const shared_ptr<Observable>&);
virtual void update() = 0;
private:
list<shared_ptr<Observable> > observables_;
};
例えば、(ObservableからObserverに送るデータ更新の)“通知”の中に、どのような情報を含めたらいいのでしょうか? 我々は、それを最小限の情報に留めました。即ち、Observerオブジェクトが知るべき情報は、データの更新があったという事だけにしました。もっと多くの情報を渡す事も可能でした(例えば、update( )メソッドの引数に“通知”を発信している Observableインスタンスを持たせるとか)。そうすれば、Observer側は何を再計算すればいいのか分るので、CPUクロックの数サイクル分、計算時間を節約できたかも知れません。しかし、それによるメリットは、その結果生じる複雑さのデメリットを上回るとは思いません。
別の疑問点として、Observerの update( )メソッドの最中に例外処理が発生した場合は、どうなるのでしょう? そのような事態は、ある Observable から“通知”を発信している最中、即ち Observable が登録している複数の Observerインスタンスに“通知”を順次発信している際に、その中のひとつの Observerインスタンスで起こり得ます。もし、そこで例外処理ルーチンに飛んでしまうと、Iteratorを使ったループ処理が中断し、残りの Observerインスタンスは“通知”を受け取れません。それは良くないですね。我々が取った方法は、何等かの例外をキャッチした場合でもループ処理は継続して行い、ループ終了後に例外処理ルーチンに飛ぶようにしました。この方法だと、発生した例外に関する情報が失われる難点がありますが、通知が中断する事と比べれば問題は小さいと考えました。
3番目の疑問点に移ります。それはコピーされた際の動作に関するものです。Observerあるいは Observableインスタンスがコピーされた際に、どのように動作するのが正解なのか、あまり明確ではありません。現状は、我々が妥当だと考える方法で実装しています。まず Observableがコピーされた場合、そこに登録されている Observerインスタンスのリストはコピーされないようにしています。一方で、Observerがコピーされた場合、コピー元が登録されていた Observableに、そのコピーも登録されるようにしています。別の方法も選択可能でしたが、実際の所、正しい選択肢は、コピーそのものを禁止する事だったかもしれません。
しかし乍ら、コピーを禁止せず上記の対応をした結果、2つの大きな問題が発生しています。ひとつ目は、Observer と Observableインスタンスの生成・消去を適切に管理しなければなりません。即ち、消去済みの Observerに“通知”が発信されないようにしなければなりません。それを実現する為、まず Observer に Observableインスタンスへの shared pointer を持たせるようにしました。それにより Observable を消去しようとしても、すべての Observer において Observable の shared pointer を消去するまでは、それが出来ないようにしました。一方で Observerインスタンスを消去する場合は、それが登録されているすべての Observableで、登録をはずすようにします。そうすれば、Observableが Observerの(スマートポインターでない)普通のポインターのリストを保持しても安全です。
ところが、この shared pointer を使った対応については、(そのままでは)シングルスレッドの場合でしかきちんと動作しません。我々は QuantLibの bindings を C# や Java に exportしようとしていますが、そこでは残念ながら、別のスレッドで常に Garbage Collection のコードが走って、不要になったHeapメモリー上のオブジェクトを消去しています。その為、Observableからの“通知”が、半分消去された(訳注:文脈から、同じ変数を指す複数の shared pointer の中の一部のポインターだけを消去した状態と想定される) Observerインスタンスに送られると、不規則なクラッシュが起こります。この問題が発見された後、この Bug は boost::shared_ptr に備わっている裏技を使って修正されましたが(Klausさん、ありがとう)、その裏技については十分な文書が無く、将来これが std::shared_ptr に移行した後も有効かどうか、よく分かりません。また、この裏技により動作スピードが遅くなっています。その為、デフォールトの設定ではこの裏技を使わないようにしており、コンパイル時のスイッチ操作で使用可能に出来る様にしています。従って、もし C# や Java で QuantLibを使う場合にだけ、使用可能にして下さい。
ふたつ目の大きな問題は(この問題は今でも続いています)、まるで Jerry Lee Lewis の歌“Whole Lotta Shakin’ Goin’ On”のように、大量の“通知”が飛び回る事です (Whole lotta notifin’ goin’ on)。ひとつのデータ更新が、何十あるいは何百もの通知発信を起動することがあります。ほとんどの update( )メソッドが、単にフラッグをセットしたり、通知を転送するだけであったとしても、数が膨らめば、時間もそれだけかかります。
QuantLibライブラリーを実際に、CVA/XVA のような、計算時間が相当かかるようなアプリケーションの中で使っている人の中には、”通知”が自動的に発信される仕組みを止めて、再計算を指示された場合にだけ明示的に行うようにする事で回避しているケースもあります(Peterさん、そうですね?)。“通知”にかかる時間を減少させる為に、通知連鎖の途中にある転送するだけのオブジェクトは飛ばしてしまう方法はうまくいきません。理由は、“通知”の連絡網の至る所で使われている Handleクラスの為です。Handleオブジェクトは re-link が可能で、オブジェクトが生成された後でも連絡網のネットワークが変更されてしまいます。
端的に言えば、この問題は未解決です。もし読者の方が良い解決方法をお持ちなら、連絡先はご存知ですよね。
A.7.2 Singleton Pattern シングルトンパターン
かの“4人組”の人たちは、彼らの本の最初の部分を creational パターン (訳注:オブジェクトの生成に関するデザインパターン。Singleton Patternの他、Abstract Factory Pattern、 Builder Pattern、Factory Method Pattern、Prototype Patternが該当。詳細は Wiki ”Creational Pattern"参照) に費やしました。論理的にはその順番になるのでしょうが、この選択は残念な副作用を生み出す事になりました。この本を読んだ熱心なプログラマー達が、それに倣って、コードのあちこちに Abstract Factory や Singletonクラスのオブジェクトをまき散らしているのを、頻繁に見かけます。言わずもがなですが、その結果、プログラムコードの明瞭さの点で意図せざる事が起こりました。
読者の方は、QuantLibライブラリーの中で Singletonクラステンプレートがあるのを見て、同じ理由によるものと疑われたかも知れません。(意地悪く言いますけど、それは見当違いです。) 幸いにも、そうで無いという根拠は、バージョンコントロールの記録で示す事が出来ます。このクラスは、QuantLibライブラリーの中では、Observerパターン (behavioral pattern)や Compositeパターン (structural pattern)よりも後に加えられました。
QuantLibライブラリーにおける Singleton のデフォールトの実装内容は下記 Listing A-28で示す通りです。
Listing A-28: Singletonクラステンプレートの実装内容
template <class T>
class Singleton : private noncopyable {
public:
static T& instance();
protected:
Singleton();
};
#if defined(QL_ENABLE_SESSIONS)
// the definition must be provided by the user
Integer sessionId();
#endif
template <class T>
T& Singleton<T>::instance() {
static map<Integer, shared_ptr<T> > instances_;
#if defined(QL_ENABLE_SESSIONS)
Integer id = sessionId();
#else
Integer id = 0;
#endif
shared_ptr<T>& instance = instances_[id];
if (!instance)
instance = shared_ptr<T>(new T);
return *instance;
}
このクラスは、Curiously Recurring Template Pattern をベースに構築しています。このパターンについては、既に Chapter VII で解説しています。あるクラスのインスタンスが Singleton(インスタンスの存在を1つしか許さない)である為には、そのクラス C を Singleton<C>クラステンプレートから派生させる必要があります。Singleton
Scott Meyers(”Effective C++, 3rd edition. Addison-Wesley, 2005”)が提言している通り、Singletonのインスタンス (訳注:原文ではinstances と複数になっている)は、instance( )メソッドの中で static変数として宣言されている map<Integer, shared_ptr> オブジェクトに保持されます (Singletonなのに、なぜ複数のインスタンスを想定しているかの説明は後ほど)。こうする事によって、いわゆる static initialization order fiasco(静的オブジェクトの初期化順序の失敗。staticオブジェクト(変数)が、それが初期化される前に他のオブジェクトで使用される時に発生する) の問題を回避し、さらに C++11 の環境では、そのオブジェクトの初期化は thread-safeになる事が保証されています。(後で見る通り、これで問題がすべて終わりという訳ではありませんが)
さて、読者の方はいくつかの疑問を持たれたかと思います。例えば、Singletonであるはずのインスタンスを、なぜインスタンスの map< > とするのか?(訳注:たった一つのインスタンスしか必要としないのに、なぜ複数のインスタンスを想定している map< > を使ってインデックスと対応付けているのか?) 実は、1つのインスタンスしか持たないと、それによる制約が発生するからです。例えば、ユーザーが、同時に異なる評価日を使って時価計算を行いたいと考えるかもしれません (訳注:時価評価日"EvaluationDate"クラスは、Singletonとして宣言されている)。そういった場合への対応として、我々はスレッド毎にひとつの Singleton を持たせる事を許容しました。その選択は、コンパイル時のフラッグにより設定できるようになっており、instance( )メソッドの中の #if 文による分岐を使って sessionID( )関数から、スレッドIDを取ってくるようにし、それを map<Integer, shared_ptr> におけるインデックス用整数として使っています。コンパイル時フラッグで、スレッド毎の Singletonインスタンスの許容を選択した場合、ユーザー自身で sessionID( )関数を実装しなければなりません。その関数は、おそらく次のようなかたちになるでしょう。
Integer sessionId() {
return /* some unique thread id from your system API */ ;
}
この関数の中で、ユーザーはOSが提供している関数(あるいはスレッドを管理用の Libraryが提供する関数)を使ってスレッドを特定し、そのスレッドID を整数に変換して返すようにします。その結果、instance( )メソッドは各スレッドに対応する Singletonインスタンスを返します。もしユーザーが、この機能をオフにすると、スレッドID は常に 0 となり、instance( )メソッドは常に同じ Singletonインスタンスを返します。その場合は、(問題含みなので)ユーザーはマルチスレッドを使いたいとは思わないでしょう。仮に使うとしても、よく注意する必要があります。理由は、前のGlobal Settingの説明をご覧下さい。
他にも、読者の方は、私が上記のListingを“デフォールトの実装内容”と述べたのはなぜかと思われるかも知れません。いい所に気づかれました。ここでは詳しく説明しませんが、実は他にも Singletonクラスの実装方法が複数あり、これもコンパイル時のフラッグ設定で選択可能です。
ひとつは、(Singletonインスタンスを保持する) map< > オブジェクトを、instance()メソッドの中で宣言される static変数とするのでは無く、Singletonクラスの staticメンバー変数として宣言する方法です (QL_MANAGED フラッグを 1 に設定すればこうなります)。これは、map< > を instance()の中で宣言した場合 .NETフレームワークの中で managed C++ としてコンパイルされると、うまく機能しない事が判明したからです (少なくとも古いバージョンの Visual Studioのコンパイラーではそうです)。
もうひとつの方法は、ユーザーがマルチスレッド環境の中でグローバルな Singletonインスタンスを1つだけ使いたい場合に使います。その場合、Singletonインスタンスの初期化は thread safe(マルチスレッド下でも安全にデータ共有できる状態)でなければなりません (ここでは Singletonインスタンスそのものの話をしており、それを保持している map<Integer, T> の事ではありません。それは new T で生成されます)。従って、この方法を選択した場合は、(thread safeを実現する為に) lock や mutex クラスなどが必要になります。デフォールトのシングルスレッドの設定の中に、これらの部品に関する記述を含めるべきではありません。従って、そのコードは、別のコンパイル時フラッグ設定の後ろにあります。興味のある方は、QuantLibライブラリーのコードを見て下さい。
読者の方の最後の疑問点は、はたして Singletonクラスがそもそも必要だったのかどうか という事でしょう。それは厳しい質問です。私見については、前に説明した Global Settingsの所を再度参照して下さい。今の所、Winston Churchillによる民主主義制度の評価と同じ様に、悪い選択肢の中で最もましなものではないかと思っています。
A.7.3 Visitor Pattern ビジターパターン
QuantLibでの Visitor Pattern の実装例を、次の Listing A-29 で示しますが、これは4人組の本による本来の形ではなく、Acyclic Visitor Pattern ( Martin “Acyclic Visitor” 1997 ) を取り入れています。そこではまず、インターフェース用の degenerateな(訳注:すなわちメソッドを持たず何の動作もしない) AcyclicVisitorクラスを定義します。それと同時に (具体的な動作を担う) Visitorクラステンプレートを定義して、その中でテンプレート引数用の動作を提供する visit( ) を純粋仮想関数として宣言しています (訳注:従ってvisit( )の具体的な動作内容は派生クラスで実装される)。
Listing A-29 : AcyclicVisitorクラスとVisitorクラステンプレートの定義
class AcyclicVisitor {
public:
virtual ~AcyclicVisitor() {}
};
template <class T>
class Visitor {
public:
virtual ~Visitor() {}
virtual void visit(T&) = 0;
};
このパターンを機能させるには、Visitorクラスの訪問を受ける側のクラス階層 (訳注:Acceptorクラス、下記コードでは Event、CashFlow、Couponクラスが該当) も必要になります。下記 Listing A-30 に、その一例を示します。
Listing A-30: Visitorから訪問を受ける側のクラスにおけるaccept( )メソッドの実装内容
void Event ::accept(AcyclicVisitor& v) {
Visitor<Event>* v1 = dynamic_cast<Visitor<Event>*>(&v);
if (v1 != 0)
v1->visit(*this);
else
QL_FAIL("not an event visitor");
}
void CashFlow ::accept(AcyclicVisitor& v) {
Visitor<CashFlow>* v1 =
dynamic_cast <Visitor<CashFlow>*>(&v);
if (v1 != 0)
v1->visit(*this);
else
Event ::accept(v);
}
void Coupon ::accept(AcyclicVisitor& v) {
Visitor<Coupon>* v1 = dynamic_cast<Visitor<Coupon>*>(&v);
if (v1 != 0)
v1->visit(*this);
else
CashFlow ::accept(v);
}
訪問を受ける側(Acceptor) のクラス階層における各クラスは(少なくとも、Visitorクラスに何等かの動作を委託しているクラスは)、AcyclicVisitorの参照を引数として取る accept( )メソッドを定義する必要があります。それぞれの accept()メソッドは、引数として渡された AcyclicVisitorインスタンスを、dynamic_castを使って、自分自身のクラスに対応する(訳注:Visitor<C> のテンプレート引数 C に、自分自身の型を指定して具体化されている) Visitorインスタンスに型変換します。その型変換がうまくいけば、その Visitorインスタンスの visit( )メソッドを呼び出します。型変換がうまくいかない場合は、代替策を探さなければなりません。仮にその Acceptorクラスが派生クラスであれば、そのベースクラスで実装されている accept( ) を呼び出し、そこで型変換を試みます。もしベースクラスでもうまく行かなかった場合は、例外処理に進みます (あるいは何もせずにメソッドを終了するという選択肢もありますが、失敗に気が付かないで終わるのは避けました)。(訳注:上記コード例では階層の一番下にある Couponクラスの accept( )内で適合する Visitor を探し、それが存在しない場合親クラスである CashFlow クラスの accept( )を呼び出して適合する Visitorを探し、さらにそれもうまくいかなければベースクラスである Eventクラスの accept()を呼び出す。それでもダメな場合は例外処理に移る)
Visitorパターンは、Chapter IV のキャッシュフロー分析の所で説明した BPSCalculatorクラスで取り入れられています。このクラスは AcyclicVisitorクラスから派生しており、従って様々なクラスの accept( )メソッドの引数として使う事が出来ます。また同時に、テンプレート引数が特定され具体化された Visitor<Class> からも派生させています (そのクラスでは visit()が実装されているはずです)。その Visitorインスタンスは、(それを必要とする)何等かのオブジェクトの accept( )メソッドに渡され、そこで(型変換が)うまくいけば visit( )メソッドが呼び出され、だめなら例外処理に飛びます。
Visitorパターンの有用性については、Chapter IVで既に説明したので、そこを参照して下さい。ここでは、なぜAcyclicVisitorパターンを選択したかについて、2点ほど説明したいと思います。
端的に言えば、4人組が提唱した元々の Visitorパターンの方が、若干プログラムのスピードは速いかも知れませんが、より取扱いが厄介です。特に、訪問を受ける側 (Acceptor側) の階層に新たなクラスを追加した場合、それに対応する visit( )メソッドを、既存の Visitorクラスの階層すべてについて追加する必要があります (そうしないとコードをコンパイルできません)。AcyclicVisitorパターンを使えば、その必要はありません。ユーザー定義の Acceptor派生クラスに対応する Visitorクラスが定義されていなくても、accept( )メソッドにおいて型変換が失敗して例外処理に飛ぶだけです。 (注: 実際の所、ユーザーは accept( )そのものを定義する必要さえありません。ベースクラスのそれを継承すればいいのです。しかしその場合は、Visitor側で、その Acceptor用の visit( )メソッドを実装しても意味がありません)
注意しておきますが、この事は(人生において多くの事がそうであるように)便利ではあるものの、良い事ではありません。新しい Acceptorをユーザー定義した場合でも、ユーザーは常に対応する Visitorクラスの階層を見直して、既存の visit( )の実装を流用する事が問題ないかチェックし、もしそうでないなら新たに visit( )を実装しなければなりません。しかし、コンパイラーがコンパイルできないデメリットと比べれば、visit( )を全ての Acceptorクラス用の Visitor階層に実装しないで済むメリットの方が大きいと思います。BPSCalculatorクラスで言えば、様々な階層の CashFlowクラスに対応する BPS計算アルゴリズムの実装が、2種類程度の visit( )メソッドの実装で十分まかなえます。
<ライセンス表示>
QuantLibのソースコードを使う場合は、ライセンス表示とDisclaimerの表示が義務付けられているので、添付します。 ライセンス