実装の詳細¶
Expressions¶
関数\(f(x_0, x_1) = 2x_0 x_1 + \cos(\sin(x_0))\)は、ライブラリを用いると以下のように書くことができる。
using tlnc::x;
using tlnc::sin;
using tlnc::cos;
auto f = 2_dc * x<0> * x<1> + cos(sin(x<0>));
この関数を\(f(1, 2)\)のように呼び出すにはヘッダtlnc/call.hpp
をincludeして以下のように書く。
boost::numeric::ublas::vector<double> args(2);
args(0) = 1.0;
args(1) = 2.0;
tlnc::call(f, args);
この関数を用いて数式の記述に関する実装の詳細を述べる。
演算子¶
演算子のオーバーロードはパターン4以外の動作をする
2変数関数と同じように定義されている。
しかし、2変数関数と違って名前空間はtlnc::expressions
に属している。
引数を表すx<0>
や関数cos
などを呼び出した結果のオブジェクトの型も
同じtlnc::expressions
名前空間内で定義されたクラスなので、
2_dc * x<0>
やx<0> + cos(x<0>)
などと書くと
ADL
によってtlnc::expressions
名前空間でオーバーロードされた演算子が呼び出されることになる。
よって、オーバーロードした演算子がtlnc::expressions
名前空間内で定義されて
is_expression
を満たすop_mul<Ts...>
のようなクラステンプレートを
型に持つオブジェクトを返すようにすれば、
式テンプレートによって数式の構造を保持することができることになり、
演算子を適用した結果を更に式の一部として利用できることになる。
定数関数¶
定数関数はtlnc::expressions::constant
クラステンプレートの
テンプレートパラメータに定数を表す型を持たせることで表す。
関数¶
どのような関数があるかはExpressionsを参照。
1変数関数¶
上の例でのtlnc::cos
やtlnc::sin
のような1変数関数に対する呼び出しは、引数の型が
is_expression
を満たすクラスis_value
を満たすクラス- それ以外
のどのパターンに当てはまるかによって結果が変わる。 どのパターンに当てはまるかはSFINAEによって判定している。 各パターンでの動作は以下の通り。
- パターン1
tlnc::expressions::cos<T>
のようなtlnc::expressions
名前空間内のis_expression
を満たすクラステンプレートに、 テンプレートパラメータとして引数の型を渡したものを型とするオブジェクトを返す。- パターン2
- コンパイル時に関数の値を評価し、その結果を数式内で扱える定数に変換する。
- パターン3
- Genericの関数を呼び出したときと同じ動作をする
2変数関数¶
冪関数pow
のような2変数関数の場合は、引数の型が
- 両方とも
is_expression
を満たすクラス - 片方が
is_expression
を満たすクラスで片方がis_value
を満たすクラス - 両方とも
is_value
を満たすクラス - それ以外
のように場合分けし、1変数の場合と同様に結果を返す。
ただし、パターン2の場合はis_value
を満たす方の引数をconstant
の
テンプレートパラメータに渡したものと同一視し、パターン1と同様の処理をする。
is_expression
を満たすクラスと、
is_expression
もis_value
も満たさないクラスを混在させた場合は、
コンパイルエラーになる。
引数¶
可変引数の変数テンプレートを特殊化し、
- 引数が1個の時は
tlnc::expressions::vector_arg<I>
- 引数が2個の時は
tlnc::expressions::matrix_arg<I, J>
- 引数の個数がそれ以外の時は0個の時に限って
tlnc::expressions::arg
が型になるようになっている。
プレースホルダー¶
tlnc::expressions::placeholder<I>
を型に持つ変数をtlnc
名前空間に
_1
から_10
まで宣言してある。
tlnc::holder<I>
は変数テンプレート。
Call¶
評価¶
Expressionsの詳細の例のように式を書くと
テンプレートが入れ子になったものがf
の型になる。
その構造を木で表すと次のようになる:
add
/ \
mul cos
/ | \ |
2 x<0> x<1> sin
|
x<0>
f
はこの木の根のadd
を指していると考える。
葉には定数か引数(x<>
やx<0>
など)かプレースホルダーしか来ないので、
関数を評価するためのタプルが渡されたときに、
- 演算子や関数は子に値を伝播させて子の評価結果に何か操作をしたものを返す
- 定数は渡されたタプルの中身に関係なく同じ値を返す
x<>
は渡されたタプルの0番目をそのまま返すx<0>
などのベクトルや行列の要素を表すものはタプルの0番目の特定の要素をそのまま返す- プレースホルダーはタプルの特定の要素をそのまま返す
というようにすれば関数の値が評価できることになる。
メモ化¶
メモの実態はタプルである。
引数が与えられたときに式の項や部分式とその評価結果の型を列挙し、
その列挙した項や部分式と評価結果をペア(std::pair
)にしてタプルに入れることでメモを作っている。
タプルを作った後は、タプルの増えた部分の評価をしてメモを更新する。
ある項をタプルに追加するとき、その項の中にある式を先にタプルに追加してからその項自身を追加するようにしている。 そうすることでメモに値を代入する際に子の中の式の評価結果を利用できるようになる。 また、タプルの要素を重複しないようにすることで同じ項を何度も評価しないようにしている。
Expressionsの詳細での例にdouble
型のベクトルが渡されたとすると、
次の表の各行をペアにしたようなものを要素として持つタプルが作成され、
左側のオブジェクトと渡されたベクトルを使って右側に具体的な評価結果を代入していく。
kv::interval<double>
のベクトルが渡されたときは表の右側が全てkv::interval<double>
になる。
項 | 評価結果の型 |
---|---|
2_dc |
double |
x<0> |
double |
x<1> |
double |
2 * x<0> * x<1> |
double |
sin(x<0>) |
double |
cos(sin(x<0>)) |
double |
2 * x<0> * x<1> + cos(sin(x<0>)) |
double |