こんにちは、高木です。

昨日は姉妹サイトに投稿したので、この連載は1回抜けました。
今後もこうしたことはときどきあるかと思いますが、何もさぼっているわけではありません。

今回は、Tcl_Obj型、厳密にいえばTcl_Obj型へのポインタをラップするobjectクラスの設計についての話題です。

昔のTclは何でもかんでも文字列で扱っていましたので、他のスクリプト言語に比べて非常に遅い印象がありました。
現在でも決して速い部類ではありませんが、データの内部表現がTcl_Objになり、バイトコードコンパイルされるようになってかなりマシになったと思います。
objectクラスはこのTcl_Obj型をラップして、より使い勝手を向上させようというものです。

まずはTcl_Obj型に対する代表的な操作について見ていくことにしましょう。

Tcl_Obj型のオブジェクトは、Tcl_New*Obj関数を使って生成します。
*のところには扱うデータの型名、具体的にはBooleanとかIntとかStringのようなものが入ります。
また、Tcl_Objから値を取り出すには、Tcl_Get*FromObj関数を使います。

データの型によって関数名が異なるというのは、ジェネリックプログラミングの大きな妨げになり、C++的ではありません。
この部分は適切に多重定義を用いて、同じシンタックスで扱えるようにしたいものです。

次に、Tcl_Obj型のオブジェクトは、内部的に保持する参照カウンタを使って寿命管理を行っています。
参照カウンタは、Tcl_IncrRefCountおよびTcl_DecrRefCount関数で増減させなければなりません。
これは非常に面倒なので、コンストラクタやコピー代入演算子で参照カウンタを増やし、デストラクタで参照カウンタを減らすようにすべきですね。

Tcl_Obj型のオブジェクトに別の値を設定するには、Tcl_Set*Obj関数を使います。
これもデータの型ごとに別の関数名になっています。
データの型ごとに関数名が変わるのも嫌ですが、それよりも、書き換え時コピーにするのか、問答無用で書き換えるのか、方針を決めなければなりません。

結論からいうと、今回は書き換え時コピーを採用することにしました。
といっても、特別なことは何もしません。
データ型ごとの代入演算子を多重定義するのをやめ、コピー代入演算子とムーブ代入演算子しか定義しないだけです。
これで、結果的に書き換え時コピーが実現できてしまいます。

Tcl_Obj型で扱える内部データには符号無し整数がありません。
素直にデータ型ごとにobjectクラスの変換コンストラクタを用意すると、unsigned int型の変数を渡そうとしたときに引数が曖昧で解決できなくなってしまいます。
文字列で”4294967295″を設定したあと、Tcl_GetIntFromObj関数でint型の値として取り出せば-1が取得できるので、値の取得はそれでもいいでしょう。
けれども、値の設定はそうはいきません。
文字列に変換したとき、結果が変わってしまうからです。

この問題への対応策には次のようなものがあります。

  1. Tclとのインタフェースでは符号無し整数は使えないと割り切る。
  2. 符号無し整数は文字列に変換して設定する。
  3. 符号無し整数を設定するときは、より大きな整数型を使用する。

どの案もいまいちなので、ここでは何もしなくていい1.案をいったん採用し、後々の課題としたいと思います。

Tcl_ObjはListという一種の配列を内部データ型として扱うこともできます。
C++のstd::vectorのようなものと考えればよいでしょうか?
当然、要素の参照や追加削除ができます。

このListをまともにサポートする必要があるのかどうか、かなり悩むところです。
この問題に取り組むには、Tclに登録したコマンドが受け取る引数について考えなければなりません。
Tcl_CreateObjCommand関数でコマンドを登録するとき、コールバックされる関数は次のようなシグニチャになります。

つまり、Tcl_Obj*の配列で受け取ることになるのです。
Tcl_Obj*の配列をobjectの配列に単純に変換することはできません。
objectクラスの中身はTcl_Obj*だけであり、仮想関数も持たないので、おそらくはsizeof(object) == sizeof(Tcl_Obj*)になります。
なので、強引にreinterpret_castで変換できなくもないのですが、いかにも不健全なコードになってしまいます。
さすがにこれは避けたいですね。

コマンドのコールバックにはラムダ式なども渡したいので、そのシグニチャに合うように、テンプレートを駆使して展開してしまうのもひとつの手です。
C++17で導入予定のstd::apply関数がヒントになるはずです。

もうひとつは、objectクラスで積極的にListをサポートするか、objectクラスを包含するList専用のクラスを使って、Tcl_Obj*の配列を見かけ上objectの配列として扱えるようにする方法も考えられます。

この問題も性急に結論を出すことはできませんので、後々の課題にしたいと思います。
ただ、テンプレートを駆使した難解なコードはなるべく避けたいのが本音です。

objectクラスはできる限り、Tclのスクリプトを組み立て、Tclの変数にアクセスし、コマンドのパラメータを受け取るだけの用途に限定したいと考えています。
込み入った処理は普通のC++の範疇で行うほうが便利ですし、効率もいいですから。

思いつくままいろいろ書きましたが、ひとつひとつのクラスについてこうした作業はついて回ります。
なるべくなら、定義するクラスは最小限にし、多少機能を制約してでも簡便なライブラリを目指したいと考えています。