実装の詳細

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::costlnc::sinのような1変数関数に対する呼び出しは、引数の型が

  1. is_expressionを満たすクラス
  2. is_valueを満たすクラス
  3. それ以外

のどのパターンに当てはまるかによって結果が変わる。 どのパターンに当てはまるかはSFINAEによって判定している。 各パターンでの動作は以下の通り。

パターン1
tlnc::expressions::cos<T>のようなtlnc::expressions名前空間内の is_expressionを満たすクラステンプレートに、 テンプレートパラメータとして引数の型を渡したものを型とするオブジェクトを返す。
パターン2
コンパイル時に関数の値を評価し、その結果を数式内で扱える定数に変換する。
パターン3
Genericの関数を呼び出したときと同じ動作をする

2変数関数

冪関数powのような2変数関数の場合は、引数の型が

  1. 両方ともis_expressionを満たすクラス
  2. 片方がis_expressionを満たすクラスで片方がis_valueを満たすクラス
  3. 両方ともis_valueを満たすクラス
  4. それ以外

のように場合分けし、1変数の場合と同様に結果を返す。 ただし、パターン2の場合はis_valueを満たす方の引数をconstantの テンプレートパラメータに渡したものと同一視し、パターン1と同様の処理をする。 is_expressionを満たすクラスと、 is_expressionis_valueも満たさないクラスを混在させた場合は、 コンパイルエラーになる。

引数

可変引数の変数テンプレートを特殊化し、

  1. 引数が1個の時はtlnc::expressions::vector_arg<I>
  2. 引数が2個の時はtlnc::expressions::matrix_arg<I, J>
  3. 引数の個数がそれ以外の時は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