2001年09月25日 豊田英司
この文章は gt4f90io の前身である gtool4 Fortran90 Tools/Library における文字列の扱いについて記されたものです。 gt4f90io は gtool4 Fortran90 Tools/Library の設計や思想、 ソースコードそのものなど多くのものを引き継いでいますが、 この可変長文字列に関しては、(主に gt4f90io の作者の力不足ゆえ) gt4f90io では考慮しないこととしています。 ただ、ソースコードをそのまま引き継いでいる部分も多いため、 可変長文字列のための構造体VSTRINGは gt4f90io 内に残っています。 そのため、参考として以下の文書をここに残しておきます。
2004年08月12日 森川 靖大
Fortran の文字型は固定長文字列です。つまり、コンパイル時に長さの決まった文字列を扱います。しかしながら、実際の文字処理ではファイル名、変数名などのように長さが実行時にならないとわからない文字列を取り扱うケースが多くあります。このようなばあい、どのようにコーディングするのがよいのかについて議論します。
Fortran の立場では、文字型はひとつの型ではなく、長さというパラメタが異なる型たちの総称です。そのため文字型変数の長さはコンパイル時に決定してしまうのです。
厳密には文字型のとりうるパラメタには長さのほかに種別 (kind) もあります。異なる種別の整数型が異なる範囲の数値を表現するように、異なる種別の文字型は異なる文字セットを表現するものとされています。しかしながら、Fortran 規格では基本文字型以外の文字型を必須としていないため、多くの処理系も基本文字型しか実装していません。本稿では移植性のあるアプリケーションを作成するという目的にかんがみ、基本文字型でない文字型について考察しません。将来 Unicode 文字型などといったものが普及してきた場合には注意する必要があります。
手続の引数は「CHARACTER(LEN=*):: 名前」のようにして呼び出し元から長さを引き継ぐことができるようになっています。この場合だけ、文字列の長さはコンパイル時に決定しません。しかしながら、その手続の実行中に長さを変えることができないという点において固定長であることは変わりません。
Fortran 95 で廃止予定になった機能ですから利用しないほうがよいのですが、外部関数に限り、関数結果の長さ引継ぎは可能です。これも上記の手続引数と同様の機能です。
長さの異なる文字型どうしの演算については以下のような規則があります。
代入文で、右辺と左辺との長さが違う場合には、右辺が短すぎる場合には文字列の後のほうを切り捨て、右辺が長すぎる場合には文字列の後に空白 (ほとんどの場合 ASCII のスペースである) を補います。
比較演算子および関数 LGT, LGE, LLE, LLT は、文字列末尾の空白はあたかも存在しないかのように比較を行います。
したがって、文字列を取り扱う際には常に文字型変数の長さを十分大きくとるようにしておけば、文字型の値を代入しても Fortran 的には同値の(つまり a == b となるような)文字列が得られることは保証されます。しかし、同値性は失われないとはいっても、印字するときなどに文字列の末尾に余計な空白が何十文字〜何百文字も表示されるのは迷惑です。このような場合のために、TRIM 組みこみ関数を用いて末尾に空白を含まない最小限の文字列を得ることができるようになっています。
このようなアプローチによるコーディングの得失を下表にまとめます。
利点 | 欠点 |
|
|
Fortran 90 (ISO 1539-1:1991) の導入後、補助規格 ISO 1539-2:1994 というものが制定され、任意の長さの文字列を表現できる type(VARYING_STRING) 型とそれにアクセスする手続の仕様を決めています。
利点 | 欠点 |
|
|
このようなアプローチでは、プログラマは扱う文字長の上限を明示する必要がありません。従って文字長の上限によってプログラムの動作に不自然な制約を課する必要がなくなります。また、プログラムの仕様変更に伴ってプログラムのあちこちを修正して回る危険もありません。このことはライブラリを自分でコンパイルしなければならない欠点を補ってあまりあるように思われます。
規格の付属書で例示された実装が「メモリリーク」する(ポインタ配列に allocate を行った後 deallocate しない)という問題は広く知られていますが、扱える文字長に上限を与える実装は禁止されていないので、固定長バッファを用いて一切 allocate を用いない実装によって、まがりなりにも規格に合致しながらメモリリークを起こさない実装を作ることができます。
注: 「まがりなりにも」というのは、処理系に扱えるプログラムの大きさあるいは複雑さに実装が上限を課することは規格上許されているため、固定長バッファを用いる実装をもって処理系とみなし、ある程度以上大きい文字列は扱えないのだと言い張れば規格合致処理系だという程度のことです。実用上は充分有益ですが、あまり誉められた話ではありません。なお、Fortran 95 準拠の新しい ISO 1539-2:1999 で例示されている ISO_VARYING_STRING モジュール実装例ではFortran 95 の構造体成分初期化機能を用いてメモリリーク問題が相当低減されています。唯一のメモリリークの可能性は VARYING_STRING 型を返す関数の結果を他の手続の引数として使用するときだけです。残念ながら、この問題だけは当面解消しない公算が大きいように思われます。
そこで、gtool4 ライブラリでは固定長バッファ方式で ISO_VARYING_STRING インターフェイスを実装してこれを内部処理で積極的に用いてきました。しかしながら、非常に残念なことに、ISO_VARYING_STRING インターフェイスには処理系のバグによる移植性の問題があることがわかってきました。
ほとんどすべての言語処理系にはバグがあるものです。そして、よくテストされていない機能にはほとんど使い物にならないほどのバグがあるものです。著者の身近な環境では、以下のような問題が発生しました(理由は推測)。
PSR Vast/F90 では文字型で固定長でない関数をコンパイルすることは出来ません。これで char(string) および char(string, length) が処理できなくなります。この問題は PSR Vast/F90 を用いないという方法で回避されており、現在知られている唯一のフリーな Fortran 90 サブセットである PSR Vast/F90 を使うことが出来なくなりました。
日立製作所の多くのコンパイラでは char(string) 関数を正しく引用できません。これは char(string, length) を用いて回避できます。
Hewrett-Packard のコンパイラには char // char // string // string をコンパイルできないものがあります。おそらく、構文解析にあたって // 演算子を何度も受理した状態では組込みの // 以外がありうるということを忘れているのでしょう。この問題はプログラムを書き換えて (char // char) // (string // string) とするか、一部の演算子を改名して char // char .CAT. string // string (ここで .CAT. のかわりに + に改名しただけではダメ)とすると回避できます。
Digital Visual Fortran 6.0 (update A) では back 引数を用いると scan, index 関数が必ず組み込みになってしまい、引数として VARYING_STRING 型が利用できません。この問題は char 関数を用いると回避できるのですが、日立コンパイラへの考慮との相乗作用で char(string, length) 形式を用いることにすると、極めて冗長なプログラムになってしまいます。むしろ、scan, index の名前を変えるのが適当でしょう。
このような問題への対処としてインターフェイスを変える場合、 (ISO_VARYING_STRING が将来有望であったとすればなおさら)インターフェイスモジュールを同名にするのは混乱の元です。
そこで、現在次のような対策が検討されています。
可変長文字列インターフェイスは各種コンパイラのバグを露見させないように変更し、名称を ISO_VARYING_STRING から DC_STRING に変更する
スタイルとして可変長文字列を使う場面を縮小する
アプリケーションプログラムが十分な移植性を確保することはソースコードレベルで流通するソフトウェアに不可欠な条件です。コンパイルを行うユーザがシステム依存な修正を適切に行えるほど確実な知識と暇を兼ね備えなければならないという要求は理不尽なものだからです。
一方、ISO_VARYING_STRING インターフェイスが顕在化させた処理系のバグは、理由が推測できるようなものでした。そこで、以下のような対策によってライブラリを修正する事にします。
モジュール名は DC_STRING にする。
型名 type(VARYING_STRING) は type(VSTRING) にする。
組込関数と同名の総称名を使わない。たとえば SCAN を VSCAN のように先頭に V をつける。
CHAR(varying_string) は実装しない。VCHAR(vstring, length) または文字型への代入文で対処する。
なお、せっかく妥協的になったついでに、もし VCHAR(vstring, length) をも廃止すれば gtool4 全体に渡って PSR Vast/F90 を利用できるのであれば、そうすべきかもしれない。要調査課題。
// の問題は微妙であるので改名は保留し、ユーザに注意を喚起するにとどめる。
もし改名するならば名称は STRCAT である。
原因となる char // char // string // char のような記述はとりたてて便利ではない。文字列の中に他の基本的な型の値を埋め込みたいという一般的な需要があるように思われるので、sprintf(3) 相当品を用意すべきである。
可変長文字列を使わなければならない目的として文字列末尾の空白を無視できない用途をあげるのはあまり現実的ではない。ISO_VARYING_STRING はそのために必要な比較演算子すら用意していないからである。してみると、可変長文字列型を用いる目的としては文字長をプログラム中にハードコードしないで済むということが最重要であると考えられる。このような見地から、以下のような指針をたてることができる。
手続内でのアルゴリズムの記述は文字型で行ったほうがよい。文字型のほうが移植性に優れているからである。
手続の INTENT(IN) 引数は CHARACTER(LEN=*) で実装するのが望ましい。
必要に応じて可変長文字列型入力引数を用いるラッパールーチンを提供してもよい。可変長文字列型が使えない場合は切り捨てられるように、組込文字型を主とすべき。
手続の INTENT(OUT) 引数は可変長文字列型で実装するのがよい。
もし INTENT(OUT) の組込文字型引数を用いる場合は CHARACTER(LEN=*) を用いて、与えられた長さでは不足な場合はエラーリターンするのが望ましい。
たいていのユーザが最大文字長を合理的に予測できるならば、組込文字型 INTENT(OUT) 引数を用いて情報を返し、長すぎる文字列の後部は切り捨てる処理をおこなってもよい。もし最大文字長が定数になるならばモジュールで定数定義すべきである。
関数返却値を文字型、あるいは可変長文字型にするのはよくないので禁止する。組込文字型ならば長さが固定長になってしまうという問題があるし、可変長文字列ならばもし ISO_VARYING_STRING の例示実装を用いた場合メモリリークしてしまう。