Wednesday, January 11, 2006

Programming::C++ - ジェネリックな逆ポーランド記法

研究でC++プログラミングをやっている中で、次のようなLISPコードの演算と同じ内容のことをしたいとか思いました。

(+ 5 (+ (+ 1 2) (+ 3 4)))

単なる二項演算なのですが、これをジェネリックにしてしまいたいと考えました。つまり数字の箇所が全てオブジェクトにしてしまいたいのです。同じくLISPで書くと

(+ obj5 (+ (+ obj1 obj2) (+ obj3 obj4)))

ですね。LISPを詳細に知らないので、obj1~obj5に数値以外の物が何でも入るのかどうかはわかりませんが…

さらに演算子+も関数オブジェクトfを使って、C++では次のことをやりたいと思いました。

f(obj5, f(f(obj1, obj2), f(obj3, obj4)))

ジェネリックに上記の計算を実現したい(且つ演算対象となるオブジェクトの数も限定しないのですが、ここでは演算順序や演算対象オブジェクト数は上記で固定して話を進めます)となると、テンプレートを使ってモリモリ書いていけばいいでしょう。このような二項演算は、逆ポーランド記法の考えとスタックを使えば、プログラムしやすいです。

例えば話を最初に戻して、数値の二項演算がしたいとすると、最初に書いたLISPコードの式は逆ポーランド記法では次のように書けます。

5 1 2 + 3 4 + + +

演算対象の項が数値の場合、数値や演算子は全てアスキーコードで表現してやると、char型のスタックを用いれば計算しやすいです。C言語のアルゴリズムを解説した書籍とかでよく(?)見かけます。

ところが、数値ではなくオブジェクトだとどうでしょうか。同じくスタックを使って、演算対象が数値だったときのアルゴリズムと同様の手法を使おうとすると困ったことが出てきます。スタックに格納する物が、演算対象となるオブジェクトと関数オブジェクトとなってしまいます。型が異なるため、少々面倒です。演算対象となるオブジェクトと関数オブジェクトに、共通のスーパークラスを持たせて、スーパークラスのポインタが格納できるスタックを用意してやれば格納できます。

// 共通のスーパークラス
class X
{
...
};

// 演算対象のクラス
class Y
 : public X
{
...
};

// 演算を行うためのファンクタ
struct Z
 : public X
{
  Y operator() (const Y& a, const Y& b)
  {
   ...
  }
};

int main()
{
  // ここにYのインスタンスや,
  // オブジェクト間の演算を行うためのファンクタZの
  // インスタンスのアドレスを格納していく
  std::stack<X*>s;
  ...
}

取り出したときにdynamic_castを使えば関数オブジェクトかどうかなどの判断も出来ます。

X* obj = s.back();
s.pop_back();
if (dynamic_cast<Z*>(obj)) {
  // obj はファンクタ
}
else {
  // obj は演算対象のオブジェクト
}

が、どうにも汚いような気がします。

結局、boost::anyを使うことで、共通のスーパークラスを用意する手間を省いて解決することにしました(実際はファンクタに渡す演算対象のクラスは同時に二種類のケースがあったので、template関数が大活躍していましたが、簡単のため上記は一種類です)。Perlを使えばスカラーリファレンスとサブルーチンリファレンスを配列にぶち込んで、ref関数でサブルーチンかどうかを判定する。これをやる手間はそんなにかからないですし。C++コードへ部分的にPerlコードを埋め込んでやりたいとか、変な衝動に駆られちゃいました。

No comments: