2. "Implementing QuantLib"の和訳
Appendix A Odds and Ends 周辺の話題
A.4 Math-related Classes 数値計算関連のクラス群
QuantLibライブラリーは、C++の標準ライブラリーが提供する算術計算ツール以外に、いくつかのツールを用意する必要があります。ここでは、それらについて概略を説明します。
A.4.1 Interpolations 補間関数
Interpolationに関する関数群は、QuantLibの中では、めずらしく使用するのが若干危険なクラスです。
ベースクラスである Interpolationクラスの概要を下記 Listing A-9 に示します。このクラスは、x と y の2つのデータ配列を使って、x 軸上の任意の点に対応する y の値を線形補間して導出します (訳注:イールドカーブで言えばxが期間、yがその期間に対応するイールド)。このクラスは、その補間された値を返す operator( )メソッドや、いくつかの便利な関数を用意しています。少し前に説明したCalendarクラスと同様、このクラスも pimpl idiom (Pointer to Implementation) を使ってpolymorphicな動作を実現しています。すなわち、まず内部クラスとして Implクラスを宣言し、その Impl の派生クラスが個別の Interpolationアルゴリズムを実装します。そして InterpolationベースクラスのメソッドからImpl派生クラスのメソッドを呼び出す形でそれを使います。もうひとつの内部クラスであるtemplateImplクラスは、一般的な動作を実装すると供に、Interpolationの対象となるデータを格納します。
Listing A-9:Interpolationクラスの概要
class Interpolation : public Extrapolator {
protected:
class Impl {
public:
virtual ~Impl() {}
virtual void update() = 0;
virtual Real xMin() const = 0;
virtual Real xMax() const = 0;
virtual Real value(Real) const = 0;
virtual Real primitive(Real) const = 0;
virtual Real derivative(Real) const = 0;
};
template <class I1, class I2>
class templateImpl : public Impl {
public:
templateImpl(const I1& xBegin, const I1& xEnd,
const I2& yBegin);
Real xMin() const;
Real xMax() const;
protected:
Size locate(Real x) const;
I1 xBegin_, xEnd_;
I2 yBegin_;
};
boost::shared_ptr<Impl> impl_;
public:
typedef Real argument_type;
typedef Real result_type;
bool empty() const { return !impl_; }
Real operator()(Real x, bool extrapolate = false) const {
checkRange(x,extrapolate);
return impl_->value(x);
}
Real primitive(Real x, bool extrapolate = false) const;
Real derivative(Real x, bool extrapolate = false) const;
Real xMin() const;
Real xMax() const;
void update();
protected:
void checkRange(Real x, bool extrapolate) const;
};
コードを見ての通り、templateImplクラスは(対象データとなる) x と y の値をコピーしていません。その代わり、この2種類の変数列に対する Iterator(ポインター)を保持して、データ値を見に行けるようにしています。これが、Interpolationクラスを使うのが安全でない理由です。(危険を回避するには)まず、Interpolationインスタンスの生存期間 が、対象となるデータ変数列のそれを越えないようにしなければなりません。すなわち、メモリーから対象データを消去した場合、(Interpolationインスタンスが持つ)そこへのポインターを残してはいけないという事です。それと同時に、Interpolationインスタンス自体を保持するオブジェクトをコピーする際は、細心の注意を払わなければなりません。
前者の注意点は、それほど大きな問題ではありません。Interpolationインスタンスが単独で使われる事は殆どありません。通常は、他のクラスのメンバー変数として、Interpolationの対象データと一緒に格納されます。その結果、Interpolationインスタンスと対象データは、(それを保持するオブジェクトの生成・消去の際に)同時に生成され、同時に消去されるので、生存期間の問題は、保持するオブジェクトが(自動的に)管理します。
後者の注意点も、さほど大きな問題ではありませんが、前者の問題は、通常、自動的に管理されるのに対し、後者の問題はユーザーが注意する必要があります。今述べた通り、Interpolationインスタンスは、通常、他のオブジェクトのメンバー変数として対象データと一緒に保持されます。そのオブジェクト用にコンパイラーが生成するコピーコンストラクターは、保持しているメンバー変数の新しいコピーも生成します。対象データについてはそれでかまいません。しかし Interpolationインスタンスの新しいコピーは、引き続きコピー元の対象データへのポインターを持つことになります(なぜなら Interpolationインスタンスのコピーは、元の対象データへのIteratorのコピーだからです)。この動作は、当然正しくありません。
これを避ける為には、ホストのクラス (コンテナーとしてInterpolationインスタンスと対象データをメンバー変数に持つクラス) の開発者は、ユーザー定義のコピーコンストラクターを作る必要があり、そこではメンバー変数をコピーするだけでなく、新たな Interpolationインスタンスを生成して、それがコピーされた対象データへのポインターを保持するようにしなければなりません。しかし、それは単純な操作ではありません。Interpolationインスタンスを保持するコンテナーオブジェクトは、その正確なデータ型の情報を持っていない(それは Implクラスの中に隠されている)ので、新しい Interpolationインスタンスを再生成できないのです。
この問題の解決方法のひとつは、Interpolationクラスに、仮想関数として、自分と同じ型のインスタンスを生成する clone( )メソッドのようなものを持たせるか、コピーされた際に、対象データへのポインターをコピーされた方の対象データへのポインターに変更する rebind( )のようなメソッドを持たせる事です。しかし、あえてそうする必要はありませんでした。なぜなら、Interpolation Traitsが既に用意されて、殆どの場合これで対応出来るからです。
何それ?と言われますよね。実は、Chapter IIIで、TermStructureクラスの Interpolationの概要を説明した際に出てきた Linear や LogLinearクラスの事です。そのひとつの例を、次の Listing A-10に示します。
Listing A-10: LinearInterpolation クラスと、その traitsクラスの概要
template <class I1, class I2>
class LinearInterpolationImpl
: public Interpolation::templateImpl<I1,I2> {
public:
LinearInterpolationImpl(const I1& xBegin, const I1& xEnd,
const I2& yBegin)
: Interpolation::templateImpl<I1,I2>(xBegin, xEnd, yBegin),
primitiveConst_(xEnd-xBegin), s_(xEnd-xBegin) {}
void update();
Real value(Real x) const {
Size i = this->locate(x);
return this->yBegin_[i] + (x - this-> xBegin_[i])*s_[i];
}
Real primitive(Real x) const;
Real derivative(Real x) const;
private:
std::vector<Real> primitiveConst_, s_;
};
class LinearInterpolation : public Interpolation {
public:
template <class I1, class I2>
LinearInterpolation(const I1& xBegin, const I1& xEnd,
const I2& yBegin) {
impl_ = shared_ptr<Interpolation::Impl>(
new LinearInterpolationImpl<I1,I2>(xBegin, xEnd,
yBegin));
impl_->update();
}
};
class Linear {
public:
template <class I1, class I2>
Interpolation interpolate(const I1& xBegin, const I1& xEnd,
const I2& yBegin) const {
return LinearInterpolation(xBegin, xEnd, yBegin);
}
static const bool global = false;
static const Size requiredPoints = 2;
};
LinearInterpolationクラスの構造は単純です。このクラスは template引数を持つコンストラクターを定義し (但しこのクラスそのものは Templateではありません)、そこで、自分用の Implクラス (LinearInterpolationImplクラス) のインスタンスを生成します。LinearInterpolationImplクラスは、templateImplクラスから継承されており、実際のInterpolationアルゴリズムの実行という重たい作業を行います (その時、ベースクラスで定義されているlocate( )クラスの助けを借りています)。
Linear traitsクラス(上記コード中の Linearクラス)は、2つの静的変数を定義しています。すなわち、Interpolationに最低限必要なデータ数が 2 である事 (requiredPoints=2)、及びある点における値を変化させた時の影響が局所的である事という事 (global=false)です。また interpolate( )メソッドを定義しており、引数で取った Iterator x と y を使って、この traitsに対応する型の Interpolationインスタンスを生成します。このメソッドは、すべての traitsで、同じインターフェース(引数のセット)を使って実装されており (仮にSpline関数のようにInterpolationにより多くのパラメータを必要とする場合は、それらの情報はtraitsのコンストラクターに渡されそこで保持されます)、先ほどのコピーコンストラクターで発生する問題の解決策も提供しています。Chapter IIIで解説した InterpolatedZeroCurveクラスのコードを見て頂くと、そこではtraitsクラスのインスタンス (そこでinterpolator_と呼ばれる変数です) の他に、Interpolationインスタンスと対象データも一緒に保持しているのが判ると思います。Interpolationインスタンスを保持するクラスについて、すべて同じ様な設計にすれば、(ユーザーが定義する)コピーコンストラクターの中で、traitsに対応する型の Interpolationインスタンスを(対象データと同時に)生成出来ます。
残念ながら、Interpolationインスタンスを保持するクラスで、このようなコピーコンストラクターをユーザーが定義する事を強制する術がありません。従って、開発者はそれを覚えておかなければなりません。Interpolationクラスをコピー不可にするか、コピーを防ぐ何等かの idiomが無い限り、これを防ぐ方法はありません。C++11では、コピー不可かつ movable に出来るので、対応可能です。
< Aside: ゴルディオスの結び目(一見複雑に見える問題をいとも簡単に解く方法) >
Interpolationインスタンスを保持するクラスを実装する際、ユーザー定義のコピーコンストラクターを作る以外に問題を防ぐ方法は、そのクラスをコピー不可にする事です。それは、見た目ほど問題ではありません。Interpolationインスタンスを保持するオブジェクトは、通常TermStructureクラスが想定されますが、大半のケースで、このクラスのインスタンスはshared_ptrsを使ってやり取りされるので、コピーコンストラクターの出る幕が無いのです。(本当の話、殆どのカーブのクラスはRelease 1.0が出るまではコピー不可になっていましたが、だれもそれについて苦情を言ってませんでした。結果的に、便宜上コピー可に戻しましたが、そうする必要があったのか、未だ確信はありません。)
最後の注意点です。Interpolationインスタンスは、対象データに対するIteratorを保持していますが、それだけではそのデータが更新された際のアップデートの仕組みとしては不十分です。対象データの更新があった場合、update( )メソッドが呼び出されInterpolationも更新できるようにするべきですが、それは(今の設計の仕組みでは)Interpolationインスタンスと対象データを保持しているクラスの役割です(但し、対象データの方は、おそらく何等かのデータソースに対しObserverとして登録されているでしょう)。しかしObserverとしての登録は、データを直接読み込んでいるInterpolationオブジェクトにも当てはまります。クラスによって実装の違いはありますが、LinearInterpolationのように、いくつかの事前計算を行い、そのデータを自分で格納しているクラスもあります。LinearInterpolationクラスの実装では、ポイント間の傾きと各ポイントの原データを、(事前計算して)格納する様になっています。対象データがどの程度頻繁に更新されるかによりますが、この事前計算の仕組みは、パフォーマンス上、最適かも知れないし、あるいは、全くその逆の可能性もあります。
(注:このクラスにあるprimitive( )とderivative( )メソッドは、若干Implementation Leakと言えるかもしれません(訳注:本来ならベースクラスで一般化したメソッドとして定義、実装されるべきものが、そこで漏れた為、派生クラスにおいてメソッドを定義・実装している)。このメソッドはInterpolateされたイールドカーブにおいて、ゼロ金利とフォワード金利の間の換算をする時に使われます。(訳注:従って、LinearInterpolationに限らずInterpolationクラス一般で使えるメソッドとした方が良かったかも知れない))
A.4.2 One-dimensional Solvers 1次元ソルバー
ソルバーは、Chapter III で説明した Bootstrappingの計算ルーティンや、Chapter IVで説明した利回りの計算などの様に、計算値を特定のターゲット値に収束させる計算アルゴリズムの中で使われます。すなわち、ある関数 \(f(x)\) が与えられ、\(f(x)=ξ\) が所定の誤差内に留まるような \(x\) を求めるアルゴリズムです。
既存のソルバーは、\(f(x)=0\) となる様な \(x\) を求めるアルゴリズムです。もちろん、これによって (訳注:ターゲット値を \(ξ\) ではなく 0 にする事によって) 汎用性を失うものではありません。但しその場合は、ユーザーが追加のヘルパー関数 \(g(x)≡f(x)―ξ\) を定義する必要があります。QuantLibライブラリーには、いくつかの(1次元)ソルバーが用意されていますが、すべて Numerical Recipes in C (W.H. Press, S.A. Teukolsky, W.T. Vetterling and B.P. Flannery, Cambridge University Press, 1992.)から取ってきて、それを書き換えて実装したものです。
(注: いくつかの多次元 Optimizer も同じ本から取ってきています。著作権の問題と同時に、C++の文法に適合させる必要もあり、若干書き換えています(例えば、配列のインデックスは0からスタートする)。)
下記の Listing A-11 は、複数のソルバーの土台として使われている Solver1D クラステンプレートのインターフェースを示しています。
Listing A-11 :Solver1Dクラスのインターフェースと、いくつかの派生クラス
template <class Impl> class Solver1D : public CuriouslyRecurringTemplate<Impl> {
public:
template <class F>
Real solve(const F& f, Real accuracy, Real guess, Real step) const;
template <class F>
Real solve(const F& f, Real accuracy, Real guess, Real xMin, Real xMax) const;
void setMaxEvaluations(Size evaluations);
void setLowerBound(Real lowerBound);
void setUpperBound(Real upperBound);
};
class Brent : public Solver1D<Brent> {
public:
template <class F>
Real solveImpl(const F& f, Real xAccuracy) const;
};
class Newton : public Solver1D<Newton> {
public:
template <class F>
Real solveImpl(const F& f, Real xAccuracy) const;
};
このクラスは、すべてのソルバーに共通して使える、複数の同一名のメソッドを提供しています。オーバーロードされた solve( )メソッドの内、ひとつは解を含むレンジの上限と下限を探します。もう一つの solve( )メソッドは、引数として渡された上限と下限の間に解が存在するかどうかチェックします。いずれのメソッドも、実際の計算アルゴリズムは、派生クラスで実装されているsolveImpl( )メソッドに委託されています。その他のメソッドは、求める解の範囲に関する‘条件’を課したり、関数の(最大)計算回数を設定したりします。
実際の計算アルゴリズムを solveImpl( )に委託するやり方は、Curiously Recurring Template Pattern(“CRTP”)を使って実装されています。この Patternについては Chapter VII で既に説明しています。ソルバーのプログラムを書いていた当時、テンプレートクラスをしきりに使おうとしていました。(その頃expression templates(Veldhuizen, Techniques for Scientific C++. Indiana University Computer Science Technical Report 2000)を実装しようとした事は、既に述べましたっけ?) 従って、このPatternを選択したのは、当時の流行に影響されたと思われるかも知れません。しかし、dynamic polymorphismを使おうとすれば、それ以外に選択肢はありませんでした。われわれは、ソルバーを、関数ポインターや関数オブジェクトと一緒に機能させたいと考えていました。またその頃は、boost::functionは存在していませんでした。その結果、template メソッドを使うに至った訳です。templateメソッドは仮想関数とする事が出来ないので、CRTPが唯一、共通のメソッドをベースクラスに置き、そこから派生クラスで定義・実装されたメソッドを呼び出す方法でした。
このセクションを終わるにあたっていくつかの注意点を述べます。一点目は、CRTPを使っている為、ユーザーがソルバーを使う関数をプログラムする場合は、テンプレート関数にする必要があります。これは少し変に感じるかも知れません。実際には、我々がプログラムしたほとんどのケースで、そんな事を気にせずに、ソルバーを最初に明示的に選択して対応していました。ユーザーの方が同じ事を行っても非難するつもりはありません。二点目は、ほとんどのソルバーは、引数として渡された関数fを呼び出して \( f(x)\) を計算するだけなので、それがどんな関数であってもソルバーは動作します。しかし Newtonクラスや NewtonSafeクラスでは、\(f.derivative(x)\) を定義する必要があります(訳注:微分不可能な関数では対応できない?)。これもまた、動的な polymorphism を使っているのに、変に感じるかも知れません。三点目は、Solver1Dのインターフェースを見ても、引数として渡される誤差の許容値 \(ε\) が、\(x\) の許容誤差(推定値 \( \tilde x\) と真の解 \(x\) との許容誤差)を意味するのか \(f(x)\) の許容誤差( \(f(\tilde x)\) の計算結果が 0 からどれだけ離れてもよいか)を意味するのか明確ではありません。これについては、既存のソルバーはすべて、\(x\) の許容誤差として使っています。
A.4.3 Optimizer 多次元関数の最小値問題
多次元の Optimizerは 1次元のソルバーより、ずっと複雑です。Optimizerの役割を掻い摘んで言うと、Cost Function(コスト関数or目的関数) \(f({\bf x})\) が最小値を返すような変数ベクトル \(\bf \tilde x\) を探し出す事ですが、その説明だけでは不十分です。それについては Cost Functionの実装内容を見ていく際に、もう少し詳しく説明します。
Optimizerのオブジェクトモデル化においては、(Solverの時とは異なり)テンプレートを使いませんでした。すべての Optimizerクラスは、次の Listing A-12に示す OptimizationMethodベースクラスから派生しています。
Listing A-12:OptimizationMethodクラスのインターフェース
class OptimizationMethod {
public:
virtual ~OptimizationMethod() { }
virtual EndCriteria::Type minimize(
Problem& P,
const EndCriteria& endCriteria) = 0;
};
このクラスが提供しているメソッドは、デストラクター以外は minimize( )メソッドのみです。このメソッドは引数として Problemクラスのインスタンスへの参照を取ります。そのインスタンスが、最小値を求める“関数インスタンス”とその“制約条件インスタンス”への参照を持ち、そこで実際の計算アルゴリズムが実行されます。計算が終了すると、多数の計算結果を返す Problemインスタンスが出来あがります。Problemインスタンスは計算結果として、最適解となるベクトル \(\bf \tilde x\) の他に、計算がどのように終了したか、その理由も返さなければなりません(最小値を探す計算がうまく収束したのか、それとも計算反復回数の最大値に到達したのでその時点での推定値を返すのか)。さらに導出された最適解 \(\bf\tilde x\) を使った関数値そのものを返す事が出来れば、良かったかもしれません(既に計算されているはずですから)。
現時点での実装は、このメソッドは計算が終了した理由を返すのみで、実際の計算結果は Problemインスタンスの中に保存されたままです(メソッドの戻り値として返さない)。これが、メソッドの引数である Problemインタンスが、non-constな“参照”として渡されている理由です。別の実装方法として、Problemインスタンスはそのままにして、計算結果をすべて“構造体”に放り込んで、それを返す事も出来きましたが、その方がより面倒くさくなると思います。一方で、(引数の Problemインスタンスだけでなく) minimize( )メソッド自体も non-constにした理由は良く分かりません。おそらく、見逃してしまったのでしょう(これについては、後程振り返ります)。
次に、下記 Listing A-13 に示す Problemクラスの説明に進みます。
Listing A-13:Problemクラスのインターフェース
class Problem {
public:
Problem(CostFunction& costFunction,
Constraint& constraint,
const Array& initialValue = Array());
Real value(const Array& x);
Disposable values(const Array& x);
void gradient(Array& grad_f, const Array& x);
// ... other calculations ...
Constraint& constraint() const;
// ... other inspectors ...
const Array& currentValue();
Real functionValue() const;
void setCurrentValue(const Array& currentValue);
Integer functionEvaluation() const;
// ... other results ...
};
既に述べたように、このクラスは、最小値を求める Cost Function(目的関数)や、制約条件や、適当な推定値、といった、最小値問題に必要な引数を取り纏めたクラスです。また、対象となる Cost Functionを呼び出しながら、同時に試行回数のカウントも行うメソッドを提供しています(それについては、Cost Functionの説明をする時に詳しく解説します)。さらに、いくつかのインスペクター関数や計算結果を取りだすメソッドも提供しています。(Optimizerの中で使われる、それらの値を設定するメソッドもあります)。
このクラスの問題点は、そういった計算で使われる要素を、non-constな参照として取り込み、保存している事です。それらを constとして取り扱うかどうかについては、後ほど説明します。それらが“参照”であること自体が問題です。なぜなら、これらのインスタンスの生成消去を少なくとも Problemインスタンスの生成消滅に合わせる責任を、ユーザー側の方で負わなければならないからです。
代替策としてOptimizerが、計算結果を構造体で返すようにする方法を取っても、若干問題は残ります(訳注:先ほどの説明に合った通り、今の実装方法は計算結果を Problemインスタンスに書き込んでいる)。もしそうするなら、Problemクラスを取り除いて、計算で使う要素を、直接 Optimizerクラスの minimize( )メソッドに渡せばいいでしょう。こうすれば、先ほどの non-constの”参照”をメモリーに保持しない為、メモリー領域の生成消去の問題は回避できます。しかし、その場合は、各 Optimizer が Cost Functionの試行回数のカウントを行う必要があり、プログラムコードを重複させる原因になります。
また、1次元ソルバーとは異なり、Cost Functionは minimize( )メソッドのテンプレート引数になっていません。各 Cost Functionは次の Listing A-14にある CostFunctionベースクラスから継承する必要があります。
Listing A-14: CostFunctionクラスのインターフェース
class CostFunction {
public:
virtual ~CostFunction() {}
virtual Real value(const Array& x) const = 0;
virtual Array values(const Array& x) const = 0;
virtual void gradient(Array& grad, const Array& x) const;
virtual Real valueAndGradient(Array& grad, const Array& x) const;
virtual void jacobian(Matrix &jac, const Array &x) const;
virtual Array valuesAndJacobian(Matrix &jac,
const Array &x) const;
};
当然ですが、このクラスのインターフェースで value( )メソッドを宣言しており、このメソッドは引数として与えられた変数の配列を使って、関数の計算結果を返します(読者の方は、ここで operator( )メソッドを宣言していない事に驚いたかも知れません)。一方で、values( )メソッドも宣言されており、これは配列を返します。Optimizerが、複数の Quoteデータを使って Calibrateするのに使われる事を思い出せば、それ程驚く事ではないでしょう。value( )メソッドが誤差値の合計(すなわち変数毎の誤差の2乗平均、あるいは同様の数値)を返すのに対し、values( )メソッドは、(入力された変数である)Quote毎の誤差値の配列を返します。この数値は、Optimizerの収束速度を向上させるようなアルゴリズムの中で使われます。
その他のメソッド群は、各変数に対する微分を返し、その値は特別な計算アルゴリズムの中で使われます。その内の一つ gradient( )メソッドは各変数に対する微分を計算し、その値を第一引数として渡された配列 (上記コードではArray& grad) に保持します。jacobian()メソッドは、同じ操作を values( )メソッドの為に行い、計算値を行列 に保持します。さらに valueAndGradient( ) と valuesAndJacobian( )メソッドは、計算誤差の値と変数に対する微分の計算の両方を同時に行います。この2つのメソッドはデフォールトとして有限差分による微分を計算しています。当然ながら相応の計算時間がかかります。仮に解析的な方法で、これらのメソッドをオーバーライドできないのであれば、差分を使った方法を使う価値があるかどうか確信が持てません。
ひとつ注意点があります。CostFunctionのインターフェースをチェックして判った事ですが、すべてのメソッドが const として宣言されています。従って、このクラスのインスタンスを Problemクラスのコンストラクターに non-const な参照として渡す事は、間抜けなやり方でした。それを const に変更するのはコンストラクターの役割を広げるだけなので、過去のバージョンとの互換性を壊す事なく変更可能でしょう。
最後に、Constraintクラスを下記Listing A-15に示します。
Listing A-15: Constraintクラスのインターフェース
class Constraint {
protected:
class Impl;
public:
bool test(const Array& p) const;
Array upperBound(const Array& params) const;
Array lowerBound(const Array& params) const;
Real update(Array& p, const Array& direction, Real beta);
Constraint(const shared_ptr<Impl ≷& impl =shared_ptr());
};
このクラスは Cost Functionの定義域に適用される“制約条件”のベースクラスです。QuantLibライブラリーは、ここでは示していませんが、いくつかの定義済みのクラスと、それらをひとつに纏めた CompositeConstraintクラスを定義しています。
このクラスの中心となるメソッドは test( )で、変数の配列を引数として取り、それらの値が制約条件を満たしているかどうかをチェックします。すなわち、配列の値が変数の領域内であれば、有効とします。このクラスは、upperBound( )と lowerBound( )メソッドを定義しており、理屈では変数の上限と下限を設定することになっていますが、実際にはそれを常に正しく設定できません。例えば領域が円であった場合、ある点のx座標とy座標が、それぞれ上限の内側にあったとしても、その点そのものは円領域の外という事が起こり得ます。
さらに注意点を2つ。ひとつ目は、Constraintクラスは update( )メソッドを定義していますが、constとして宣言されていません。もしこのメソッドの目的が制約条件自体を変更するなら、そうする意味があったでしょうが、そうでないなら問題です。実際には、このメソッドは変数の配列とその方向の配列を引数として取り、変数が制約条件を満たすよう、与えられた方向に値を修正していきます。(訳注:すなわち制約条件自体を変更するのではなく、引数である入力変数を変更している) 従って、このメソッドは const にすべきでしたし、メソッド名も別のものにすべきでした。たとえ子供がと駄々をこねるように、開発者が“I can’t”と言ったとしても。実際には、なんとか修正出来たでしょう。
ふたつ目は、このクラスは pimpl idiom(Pointer to Implementation) を使っています(これについては既に説明しています)。デフォールトのコンストラクターは任意の引数として Implインスタンスに対するポインターを取っています。もし今このクラスを再設計するとしたら、まず引数を取らないデフォールトのコンストラクターを作成します。その上で別途 Implへのポインターを取るコンストラクターを作成し、それを protected として宣言して派生クスでしか使えないようにしたでしょう。
最後に、これらのクラスがconstを正しく使ったかどうかについて私の考えを短く説明します。端的に言えば、良くありません。いくつかのメソッドは修正可能ですが、いくつかは無理です。例えば、minimize( )メソッドを変えると、過去のバージョンとの互換性が失われるでしょう(このメソッドは純粋仮想関数として宣言されておりconstかどうかという事はメソッドの属性に含まれています)。また、いくつか他のOptimizerクラスもminimize( )メソッドを通して別のメソッドを呼び出し、メソッド間のデータのやり取りを、メンバー変数を使って行っているので、同じく互換性の問題が起こります。
(注:いくつかのOptimizerクラスではメンバー変数をmutableで宣言しているものがあります。おそらく、かつてはメソッドがconstとして宣言されていた為でしょう。このセクションを書いている時点では、詳しく調べていませんが)
Version1.0がリリースされる前に、コードを見直す努力をもう少ししていれば、この問題を避けられたかもしれません。若いプログラマーの方は、これを反面教師として学んで下さい。
<ライセンス表示>
QuantLibのソースコードを使う場合は、ライセンス表示とDisclaimerの表示が義務付けられているので、添付します。 ライセンス