|
本サイトは移転しました。旧アドレスからのリダイレクトは2025年03月31日(月)まで有効です。
|
🛈 | ✖ |
ファイルストリームバッファに用いる文字コード変換ファセット(codecvtファセット)クラスを自作する。
iconvライブラリを利用し(iconvが扱う)任意の文字コード間の変換を一般に扱うクラステンプレートとして、以下の二つに対応する。
iconvはC++標準ではないがPOSIX標準で互換性は高い。本サイトプログラミング環境ではMSYS2でコンパイラと同時にiconvライブラリも導入していてそのまま利用可能となっている。インクルードファイル、ライブラリファイル共に標準ディレクトリにあり、インクルードファイル(iconv.h)とライブラリファイル(libiconv.aまたはlibiconv.dll.a)の指定だけで良い。作成したクラスはクラステンプレートあるいは名前空間stdクラステンプレートの部分特殊化なので、ヘッダオンリーライブラリ(インクルードファイルのみ)として利用できる。
本記事の内容はcodecvtを考察するで詳細な議論を追加する。提案した自作codecvtには使用上の制限と不具合があり、そちらで確認いただきたい。さらに本記事の最後に提示する疑問点について、本サイトの解答も示している。
以下により構成される。
ライブラリ利用スキーム1と2で共用するクラステンプレートで、codecvtクラステンプレートの各仮想メンバ関数(do_in、do_outなど)を実装する。パブリックメンバ関数はそれぞれcodecvt仮想メンバ関数からコールされる事を想定し、名称が対応を明示する。テンプレート仮引数Tが内部文字コードを指定し、Uが外部文字コードを指定する。文字コードを指定するクラスは例えば以下である。
iconvライブラリはiconv_t型の変換記述子で変換を管理する。TMyCodeCvtStateIconvは二つの変換記述子を持ち入出力それぞれに割り当てる。iconv_t型はvoid*にtypedefされていてデリータ定義のstd::shared_ptrスマートポインタで保持する。TMyCodeCvtStateIconvはライブラリ利用スキーム1でcodecvtクラステンプレート仮引数stateTの実引数として使用する。これをファイルストリームバッファクラステンプレート(basic_filebuf)が利用するには、TMyCodeCvtStateIconvを後述のtraits::state_typeに指定する。traits::state_typeは以下を要件とする(N4659 24.2.2/p4)。変換記述子をshared_ptrに保持させる理由は、コピー代入/構築で変換記述子を二重に閉じないための処置である。
ライブラリ利用スキーム1を実装する。codecvtクラステンプレートの特殊化として、std名前空間内のcodecvtクラステンプレートの第三仮引数stateTをTMyCodeCvtStateIconvクラステンプレートとする部分特殊化を行う(20.5.4.2.1/p1)。なお将来拡張に備えクラステンプレートもテンプレートテンプレート仮引数で受ける。stateTは相互に変換される文字コードペアを選択する(N4659 25.4.1.4/p2)。stateTはその参照が各仮想メンバ関数の第一仮引数stateであり、stateは"変換状態"の記憶に用いることができる(Nicolai M. Josuttis, The C++ Standard Library, Boston, Addison-Wesley, 1999; Boston, Addison-Wesley, 2004, p.721、Angelika Langer et al., Standard C++ IOStreams and Locales, Reading, Addison-Wesley, 2000, p.282)が規格は何も定義しない(25.4.1.4/p3、25.4.1.4.2/p4)。basic_filebufクラステンプレートは文字コード変換に以下のa_codecvtを用いる(30.9.2/p5)。traitsはクラステンプレート第二仮引数で、所有されるストリームクラステンプレートの仮引数を受け継ぐ(30.9.3.1/p2、30.9.4.1/p2、30.9.5.1/p2)。
use_facet関数テンプレートはパブリックスタティックメンバ変数idでlocaleの所有するファセットを検索する。codecvt部分特殊化の特殊化はそれぞれ独自のid値を持つ(25.3.1.1.2/p1、25.3.1.1.3/p1)。TMyCodeCvtStateIconv特殊化のインスタンスはクライアントが所有し、パブリックメンバ関数を通し仮想メンバ関数第一仮引数stateの実引数として参照され、文字コード変換はstateが行う。
codecvt部分特殊化はmingw-w64標準ライブラリ実装の__codecvt_abstract_baseクラステンプレートを継承することでパブリックメンバ関数の定義を省略している。__codecvt_abstract_baseが得られない場合は全て自ら定義しなければならない。
ライブラリ利用スキーム2を実装する。TMyCodeCvtクラステンプレートは任意のcodecvt特殊化を置き換えるが、主な目的はstateTをmbstate_tとしたライブラリ標準codecvt特殊化(N4659 25.4.1.4/p3)の置き換えにある。TMyCodeCvtは置き換えられるcodecvt特殊化を継承し、仮想メンバ関数をオーバーライドするがid値は維持する(25.3.1.1.2/p1、25.3.1.1.3/p1)。テンプレート仮引数myStateTはTMyCodeCvtStateIconvクラステンプレートの特殊化を想定し、そのインスタンスをTMyCodeCvtはメンバ変数myState_として持ち、文字コード変換はmyState_が行う。仮想メンバ関数第一仮引数stateは全く用いられない。なお、codecvtファセットにおけるメンバ変数使用の問題点について後に考察を加える。
TMyCodeCvtStateIconvクラステンプレート仮引数に文字コードを指定する実引数として与えるクラスを定義する名前空間。名前空間を使用する必然性は無いが、グローバル名前空間の不必要な汚染を避けるため導入した。ENCODING_TRAITSはクラス定義を支援するマクロで形式的にNMyEncoding名前空間内に記述したが、名前空間とは無関係の存在である事は言うまでもない。
iconvの扱える文字コードの内のUTF-16LE、UTF-8、CP932の非常に単純なケースでのみ動作確認している。以下ではワイド実行文字コードはデフォルト(UTF-16)のままでコンパイルされていて、実行パスにUTF-8で記述された"input_utf8.txt"があるものとして、内部文字コードUTF-16と外部文字コードUTF-8の変換を確認する。"input_utf8.txt"にはホワイトスペースで区切られた数字の文字列が書かれているものとして、整数型変数との入出力を行う。以下においてTMyStateはTMyCodeCvtStateIconv<NMyEncoding::UTF16,NMyEncoding::UTF8>のエイリアスとする。
codecvtファセットにcodecvt<wchar_t,char,TMyState>を用いる。これはcodecvtクラステンプレートのTMyCodeCvtStateIconvクラステンプレートによる部分特殊化の、TMyStateによる暗黙的特殊化である。
TMyTraitsクラスをbasic_filebufクラステンプレートの第二仮引数traitsに指定する。TMyTraitsはstate_typeの他にpos_type(N4659 24.2.2/p2)をfpos<TMyState>と定義しないとstatic_assertエラーでコンパイルできない。fposクラステンプレートのデフォルト(プライマリテンプレート)はstreamoff型メンバ変数への処理が主でテンプレート実引数へ依存せず(include\c++\14.1.0\bits\postypes.h:70)、つまりプライマリテンプレートによる暗黙的特殊化で良い。
basic_filebuf<wchar_t,TMyTraits>を使用するストリーム(通常はbasic_ifstream<wchar_t,TMyTraits>、basic_ofstream<wchar_t,TMyTraits>、basic_fstream<wchar_t,TMyTraits>)はこのcodecvtファセットで内部文字コードUTF-16、外部文字コードUTF-8で文字および文字列の入出力を行う。ただし文字列型はwstringではなくbasic_string<wchar_t,TMyTraits>となる。これだけでは文字および文字列以外のデータ(例えば数値)入出力に失敗するが、その理由はlocaleのデフォルト所有するファセット(25.3.1.1.1/p3 Table69)では不足なためである。例えばデフォルト所有のnum_put<wchar_t>は実際はnum_put<wchar_t,ostream_iterator<wchar_t,mbstate_t>>なので(25.4)、basic_ofstream<wchar_t,TMyTraits>がテンプレート実引数に従いnum_put<wchar_t,ostream_iterator<wchar_t,TMyTraits>>を利用しようとしても得られない。サンプルコードはnum_putとnum_getのみ対応し通常の数値データ入出力を可能としたが、その他にmoney_put、money_get、time_put、time_getが必要である。
codecvtファセットとしてTMyCodeCvt<TMyState>を用い、ライブラリ標準codecvt<wchar_t,char,mbstate_t>に置き換える。これはTMyCodeCvtクラステンプレートのTMyStateによる暗黙的特殊化である。
サイト作成者は以下の疑問を持つ。一応の解答を別記事で示したが、より正確な解答をご存知であれば教示いただきたい。
仮想メンバ関数do_in、do_outは(from_end-from)個以下の変換元(source)符号を変換し(to_end-to)個以下の変換先(destination)符号を変換先へ収納する(N4659 25.4.1.4.2/p2)。[from,from_end)全てが変換できなかった(from_next<from_end)場合は値partialを返す(25.4.1.4.2/p5)。from_next==from_endでpartialを返す場合は変換された符号列が変換先符号列に全て収納されていない、あるいは変換先符号の生成にはさらなる変換元符号列の追加が必要(部分符号シーケンス)である事を示す(25.4.1.4.2/p5)。変換できない文字に遭遇した場合はfrom_nextとto_nextを変換成功した符号の次を指したまま(25.4.1.4.2/p2)値errorを返す(25.4.1.4.2/p5)。
部分符号シーケンスとなる場合の実装は二つのスキームが考えられる。
ライブラリ利用スキーム1と2が共用するTMyCodeCvtStateIconvクラステンプレートは部分変換スキーム2によるが、教科書は部分変換スキーム1(Nicolai M. Josuttis, The C++ Standard Library, Boston, Addison-Wesley, 1999; Boston, Addison-Wesley, 2004, p.722)と部分変換スキーム2(Angelika Langer et al., Standard C++ IOStreams and Locales, Reading, Addison-Wesley, 2000, p.284)で対立する。部分変換スキーム2では、変換先がバッファで符号列長が1文字の符号長よりも短い(例えば2符号長文字なのに1符号長しかない)場合に無限ループに陥ると予想される(1符号長でも収納さえできればバッファとして機能できるのに)。
テンプレート仮引数stateTの参照が各仮想メンバ関数(do_in、do_outなど)の第一仮引数stateであり、stateは変換状態記憶に用いることができるが規格は何も定義しない(N4659 25.4.1.4/p3、25.4.1.4.2/p4)。変換状態記憶は以下の二つのスキームが考えられる。
ライブラリ利用スキーム1は状態記憶スキーム1であり、ライブラリ利用スキーム2は状態記憶スキーム2である。ライブラリ利用スキーム2はライブラリ標準codecvtファセット(25.4.1.4/p3)を置換するためstateTはmbstate_tに固定される。これを状態記憶スキーム1で実装するにはTMyCodeCvtStateIconvクラステンプレート特殊化(前述サンプルコードではTMyState)のインスタンスをmbstate_t型stateに押し込むことになるが、そもそもmbstate_tが実装依存で一般解が存在しない。mingw-w64標準ライブラリ実装に限ればmbstate_tはint型で、かつて類似のケースで状態記憶クラスのポインタをキャストしてstateに保持させるという危険な実装を見かけた記憶があるが、64ビットのポインタ型はint型サイズの2倍でこれも不可能となった。もう少し頑張れるなら、状態記憶クラスの配列を用意しstateにそのインデックスを保持させても良いかもしれない。
複数のファイルストリームバッファがcodecvtファセットを共有する場合を考える。状態記憶スキーム1はファイルストリームバッファ毎に変換状態を記憶し問題ないが、状態記憶スキーム2はクラスメンバ変数(例えばmyState_)に記憶を共有する。部分変換スキーム2ならmyState_は部分符号シーケンスの記憶が不要で一見安全だが、JIS(ISO-2022-JP)などステートフルな文字コードはシフト状態も記憶して共有はエラーを招く。にもかかわらず前述サンプルコードはfinとfoutでTMyCodeCvt<TMyState>を共有するが、入出力に異なった変換記述子を割り当てたまたま安全なだけであり、状態記憶スキーム2は一般には共有できない。
ファイルストリームバッファ(basic_filebuf)は内部型から外部型へのマッピングに1:N(1個の内部型符号をN個の外部型符号にマッピングする)を仮定する(N4659 25.4.1.4.2/p3および脚注236)ため、答えは明らかに否であろう。加えて外部型のサイズは8ビットに固定される(30.9.2/p5)。これでは使用できる文字コード変換は大きく制約され、特にウィンドウズアプリケーションでは内部型は事実上UTF-16なので完全に規格合致したファイルストリームバッファ入出力は望めない。なぜならUTF-16からUTF-8(あるいはシフトJIS、あるいは...)への文字コード変換はN:Mとならざるを得ないためである。
この制約はcodecvtファセット自体の制約でなく、basic_filebufの使用できるcodecvtファセットの制約である。本サイトはしかし制約にとらわれる事なく以下のcodecvtファセットをbasic_filebufで使用する。
これらは全て文字コード変換N:Mであるが、少なくとも本サイトのサンプルコードでは問題となっていない。