Exceptional C++を焚き付けにする話

引きずって申し訳ありませんが、先のranhaさんの発表を聞いて以来、Exceptional C++を読み返しています。
で、116ページでとんでもない文を見つけてしまいました。

先行宣言で十分な場合は、決してヘッダをインクルードしないこと。

とんでもない!この一文だけでこれは禁書です、燃やしましょう!

……ええ、ええ、説明しますとも。
私がCのヘッダファイルのトランスレータを作っていることは前に書いたとおりです。変換できるライブラリも割と順調に増えて、そろそろ標準Cライブラリはカバーできるのではないかってあたりは視野に入ってきました。最終目標のwindows.hはまだまだ遠いですが。

で、今libxml2対応しようとして「どうしようか、これ……」ってなってるところです。具体的には次のようなパターン。

one.h

struct T;
typedef struct T *t_ptr;

two.h

#include "one.h"
struct T { t_ptr mem; };

one.hでは、struct Tをopaque typeとして宣言して、そのポインタだけを使ってます。全く問題ありません。

two.hでは、struct Tに実体を与えて、そこで自身のポインタをメンバに持ってます。全く問題ありません。

問題は、two.hがone.hを#includeしているため、必ずone.hがtwo.hより先に来ることです。
今まで変換に成功した他のライブラリにもこの手のパターンはあったのですが、幸いにもtwo.hがone.hを#includeしてはいなかったため、#includeの順番を調整することで回避できました。これは直接#includeが書かれてしまっていて順番を入れ替えることができません。

……ええ、ええ、意味不明ですね。

C言語では、opaque typeとその実体は依存関係なく別々に宣言可能です。これははっきり言って珍しい機能であり、C言語特有の超便利な機能です。もっと言えばC言語がモジュールを持たないからこそ実現できている機能です。

まともなモジュールがある言語でこれを表現するには……循環参照が必須ですよね、ですよね。
んでもってまともなモジュールがある言語はフツー公開部分の循環参照が禁止されてますよね、ますよね。

どうしろと!?

D言語は確か循環参照できたよなー(うろ覚え)……とかそういう問題ではなくて。
まともなモジュールがある言語では、struct Tの宣言箇所を1箇所に絞らないといけないわけですよ。
つまりopaque typeの宣言と実体の宣言が別ヘッダにあることを検出して、内部的に循環参照に置換しないといけないわけです。
(実体の宣言をtwo.hからone.hに移動するというのはダメです。実体はtwo.hで宣言されている他の何かを使ってるかもですし、opaque typeを宣言してるのはone.hだけじゃなくもっとあるかもしれませんし)

one.d

import two;
alias struct_T* t_ptr;

two.d

import one;
struct struct_T { t_ptr mem; }

Adaの場合はもっと面倒です。普通には循環参照できないので、どっちかにlimitedを付けないといけません。
limitedを付けた参照先はaccess型(ポインタ)経由でしか扱えなくなりますので、one→twoとtwo→oneのどっちに付けるか判定が必要です。
更にAdaはCのようなやわらか型言語とは異なりvery very strong-typingな言語ですので、複数ヶ所でaccess型(ポインタ)を宣言するとそれぞれ別の型になってしまいます。
これを避けるために私のトランスレータではポインタ型の宣言は元の型と同じ場所に固めることにしています。
つまり本当であればstruct T *やstruct T **に相当する宣言はtwoに入ります。で、ポインタのtypedefみたいなのは、「元の型と同じ場所(two)で宣言されてるポインタ型」の別名として定義するようにしてます。
limited withしますとこれが使えなくなります。ポインタと言えどもlimited with先の型を値として使おうとしていることには変わらないからです。
つまり元の型に対するポインタ型を2箇所で宣言する羽目になり、strong-typingな言語ではこれが結構痛いわけです。

one.ads

limited with two;
package one is
   type t_ptr is access all two.struct_T;
-- subtype t_ptr is two.struct_T_ptr; ← エラーになる
end one;

two.ads

with one;
package two is
   type struct_T is record mem : one.t_ptr; end record;
   type struct_T_ptr is access all struct_T; ← struct T*が使われているので自動生成
end two;

↑のone.t_ptrとtwo.struct_T_ptrに互換性はありません!

追記: twoからはoneは全部見えてるんだから全部one.t_ptrを使うようにしてしまえば宜しいのでした。あほか私は……。

この時点でもう実装をあきらめたいレベルなわけですが、これにインライン関数でもくっついてて、struct T::memに触られてたりすると……。

このトランスレータは今のところAdaしかサポートしてませんが、最終的は汎用にしたいわけです。循環参照ができない言語ですと(Dなんかが例外であってほとんどの言語がそうでしょう)……one.h内で使われている分はvoid *扱いにでもするしかないですよねもう。

というわけで、先行宣言を別のヘッダファイルに泣き別れにするのはやめてください、
先行宣言は同じファイル内だけで完結させてください、お願いします!

(理由: 他の言語に移植できなくなるからC言語を滅ぼす日が遠くなる)

っていうかostreamの宣言がclass直からtemplateになったためにができたってすぐ前のページで書いてるんですから、自前の型も後でtemplateにするかもしれないわけですから、先行宣言をあちこちに書き散らかすなんて推奨するのはやめましょうよHerb大先生!これがありなら関数のプロトタイプだって使う場所で別個に書けば#include要らないよねやったーって話になるじゃないですかー。