|
本サイトは移転しました。旧アドレスからのリダイレクトは2025年03月31日(月)まで有効です。
|
🛈 | ✖ |
C++標準ライブラリのcodecvtファセットについてサイト作成者の理解を整理する。
codecvtはlocaleインスタンスが保持するファセットの一つである。localeインスタンスは様々なファセットを一括して任意のストリーム(basic_stream)に設定(imbue)する。ストリームはlocaleインスタンスを通して各ファセットを利用する。codecvtを利用するストリームはbasic_fstream(ifstream、wifstream、ofstream、wofstream)、より正確にはストリームの所有するbasic_filebuf(filebuf、wfilebuf)に限定され、これはストリームを限定しない他のファセットと大きく異なる。codecvtは文字コードを変換する。主たる目的はファイル入出力での利用だが、標準唯一の文字コード変換フレームワークとしてその他の目的にも利用できる。
本記事はロケールとファセット、とりわけcodecvtファセットの文字コード変換ライブラリ利用による自作を議論する。アプリケーションでのそれぞれの利用方法は日本語アプリケーションをどのように開発するかなどを参照いただきたい。
ロケールは言語や地域を定義するパラメータセットで、インターフェースはこれを用いてユーザー最適な方法で情報交換する。
mingw-w64標準ライブラリの実装を確認する。
C++標準ライブラリはロケール(JTC1/SC22/WG21 N4659 25)をlocaleクラス(25.3)で扱い、そのインスタンスはパラメータセットを複数のファセットで構成する(C++ロケール)。ファセットはC言語(JTC1/SC22/WG14 N1570 7.11)/POSIX(IEEE 1003.1-2017 7. Locale)のカテゴリを概ね踏襲し、いずれかのカテゴリ(N4659 25.4)に属するクラステンプレートで表す。全てのクラステンプレートは文字型を第一のテンプレート仮引数に受ける。いくつかのクラステンプレートはサフィックス_bynameの付く派生クラステンプレート(例えばctype_byname)を持ち、ロケール名(後述のstd_name)をコンストラクタの仮引数に受ける(25.3.1.1.2/p5、25.4.1.2、25.4.1.5、25.4.3.2、25.4.4.2、25.4.5.4、25.4.6.4、25.4.7.2)。
カテゴリ | C言語/POSIX | 説明 | ファセット/クラステンプレート |
---|---|---|---|
ctype | LC_CTYPE | 文字分類と大小文字変換 | ctype(25.4.1.1), codecvt(25.4.1.4) |
numeric | LC_NUMERIC | 通貨以外の数値形式 | numpunct(25.4.3.1), num_get(25.4.2.1), num_put(25.4.2.2) |
collate | LC_COLLATE | 照合順序 | collate(25.4.4.1) |
time | LC_TIME | 日付時間形式 | time_get(25.4.5.1), time_put(25.4.5.3) |
monetary | LC_MONETARY | 通貨型式 | moneypunct(25.4.6.3), money_get(25.4.6.1), money_put(25.4.6.2) |
messages | LC_MESSAGES | 情報/診断メッセージ形式 | messages(25.4.7.1) |
localeのコンストラクタを整理する(25.3.1.2)。categoryはカテゴリを示す整数型である(25.3.1.1.1)。
コンストラクタ | 構築するコピー |
---|---|
locale() | グローバルC++ロケール |
locale(const locale& other) | other |
locale(const char* std_name) | std_nameを名前とするC++ロケール |
locale(const std::string& std_name) | |
locale(const locale& other, const char* std_name, category cat) | otherにstd_nameロケールのcatカテゴリ導入 |
locale(const locale& other, const std::string& std_name, category cat) | |
template<class Facet> locale(const locale& other, Facet* f) | otherにfファセット導入 |
locale(const locale& other, const locale& one, category cat) | otherにoneのcatカテゴリ導入 |
グローバルC++ロケールはlocale::globalスタティックメンバ関数が設定するが、単なるlocaleインスタンスに過ぎずグローバルな挙動に直接影響する物ではないため、"グローバル"は誤解を招く呼称だと思う。なおC言語標準ライブラリとの関係には注意が必要で後述する。
localeインスタンスは以下の標準ファセットまたはその派生を必ず所有する(N4659 25.3.1.1.1/p3)。localeコンストラクタのcatカテゴリが導入するファセットは、最低でもこれらの中でそのカテゴリに該当するものを含む(25.3.1.1.1/p2)。なおC++20はchar8_tの追加でcodecvt<char16_t,char,mbstate_t>とcodecvt<char32_t,char,mbstate_t>は非推奨となり、codecvt<char16_t,char8_t,mbstate_t>とcodecvt<char32_t,char8_t,mbstate_t>に代替された(N4849 28.3.1.1.1/p2)。
カテゴリ | ファセット |
---|---|
collate | collate<char>, collate<wchar_t> |
ctype | ctype<char>, ctype<wchar_t>, codecvt<char, char, mbstate_t>, codecvt<char16_t, char, mbstate_t>, codecvt<char32_t, char, mbstate_t>, codecvt<wchar_t, char, mbstate_t> |
monetary | moneypunct<char>, moneypunct<wchar_t>, moneypunct<char, true>, moneypunct<wchar_t, true>, money_get<char>, money_get<wchar_t>, money_put<char>, money_put<wchar_t> |
numeric | numpunct<char>, numpunct<wchar_t>, num_get<char>, num_get<wchar_t>, num_put<char>, num_put<wchar_t> |
time | time_get<char>, time_get<wchar_t>, time_put<char>, time_put<wchar_t> |
messages | messages<char>, messages<wchar_t> |
std_nameを与えて構築したlocaleインスタンスは_bynameの付くバージョンにstd_nameを渡して構築した派生ファセットを所有する。mingw-w64を含むほとんどの実装でstd_nameが"C"は"C"ロケール(最小環境)、空文字列""はネイティブ環境、"POSIX"は"C"と同義である(N4659 25.3.1.2/p4、N1570 7.11.1.1/p3)。ネイティブ環境はPOSIX準拠としてLANG環境変数に従う(IEEE 1003.1-2017 8.2 Internationalization Variables)が、mingw-w64ではC言語標準ライブラリとの関係で問題を生じる。グローバルC++ロケールのデフォルトは"C"ロケールである。mingw-w64実装はstd_nameに"C"と"POSIX"のみを許容し、LANG環境変数が未定義か"C"あるいは"POSIX"の場合のみ""を許容する。これ以外は実行時に例外を送出する。_bynameの付くファセットを"C"または"POSIX"以外で直接構築しても同様である。この制限はmingw-w64がウィンドウズネイティブである事が理由ではなく、msys2サブシステムやCygwinのGCCも同じ制限を持ち、GCCのウィンドウズ実装共通の問題らしい。
異なるテンプレート実引数による特殊化(例えばnum_get<int>)はcatカテゴリに含まない。
結論として、mingw-w64は"C"以外のC++ロケールを名前で構築できず、"C"ロケールへのfファセット導入が唯一の変更手段となる。この制限はそれほど致命的でなく、第一に日本語コンソールアプリケーションでも"C"ロケールによる入出力が一般的で、第二にデスクトップアプリケーションならそもそもそういった入出力が不要で、第三に必要ならwxWidgetsなどのライブラリが同等の機能を供給する。こういった理由で本サイトはロケール変更に強い動機を持たないが、codecvtファセットを例外として文字コード変換に重用する。
参考として、C言語標準ライブラリのロケール(C言語ロケール、"C"ロケールと混同しないように)を確認する。C言語ロケールはsetlocale関数により名前でグローバルに設定する(N1570 7.11.1.1)。strftime関数などのロケール依存関数はこの設定で挙動が変わる。
mingw-w64を含むGCCウィンドウズ実装は既述のように"C"以外のC++ロケールを名前で構築できない一方で、setlocaleはC言語ロケールを変更できる。setlocaleはグローバルC++ロケールを変更しないが、locale::globalは名前付きC++ロケールを与えた場合にグローバルC++ロケールとC言語ロケールを変更して(N4659 25.3.1.5/p2)、両ロケールを一括する唯一の方法とする(Nicolai M. Josuttis, The C++ Standard Library, Boston, Addison-Wesley, 1999; Boston, Addison-Wesley, 2004, pp.697-698)。しかしmingw-w64はそのようなC++ロケールを与える事が不可能であり、結局locale::globalはC言語ロケールを変更できない。
mingw-w64(ウィンドウズネイティブ)とmsys2サブシステムGCC(POSIX依存)で確認する。setlocaleをコールして日付時間をstrftimeで書式化して出力する。出力はコマンドプロンプト(シフトJIS)とPOSIX互換ターミナル(UTF-8)で確認した。
コンパイラ | ロケール名 | setlocaleの戻り値 | コマンドプロンプト(シフトJIS) | POSIX互換ターミナル(UTF-8) |
---|---|---|---|---|
mingw-w64 | en_US | (null) | ||
de_DE | (null) | |||
ja_JP.sjis | (null) | |||
ja_JP.eucjp | (null) | |||
ja_JP.utf8 | (null) | |||
english_usa | English_United States.1252 | 1/27/2022 9:00:00 AM | 1/27/2022 9:00:00 AM | |
german_germany | German_Germany.1252 | 27.01.2022 09:00:00 | 27.01.2022 09:00:00 | |
japanese_japan.932 | Japanese_Japan.932 | 2022/01/27 9:00:00 | 2022/01/27 9:00:00 | |
japanese_japan.20932 | Japanese_Japan.20932 | 2022/01/27 9:00:00 | 2022/01/27 9:00:00 | |
japanese_japan.65001 | (null) | |||
msys2サブシステムGCC | en_US | en_US | Thu, Jan 27, 2022 9:00:00 AM | Thu, Jan 27, 2022 9:00:00 AM |
de_DE | de_DE | Do, 27. Jan 2022 09:00:00 | Do, 27. Jan 2022 09:00:00 | |
ja_JP.sjis | ja_JP.sjis | 2022年01月27日 09時00分00秒 | 2022▒N01▒▒27▒▒ 09▒▒00▒▒00▒b | |
ja_JP.eucjp | ja_JP.eucjp | 2022年01月27日 09時00分00秒 | 2022ǯ01▒▒27▒▒ 09▒▒00ʬ00▒▒ | |
ja_JP.utf8 | ja_JP.utf8 | 2022年01月27日 09時00分00秒 | 2022年01月27日 09時00分00秒 | |
english_usa | (null) | |||
german_germany | (null) | |||
japanese_japan.932 | (null) | |||
japanese_japan.20932 | (null) | |||
japanese_japan.65001 | (null) |
mingw-w64とmsys2サブシステムGCCは使用できるロケール名が異なり、前者はマイクロソフトに準拠し後者はPOSIXに準拠する。前者はしかしマイクロソフトのドキュメントする全てが使えるわけではなく(例えば"ja-JP"が使えない)、理由はmingw-w64のランタイムがmsvcrt.dll(MSVCRT)である一方、ドキュメントは新しいユニバーサルCランタイム(UCRT)で書き換えられているためと思われる。同じ理由でmingw-w64のsetlocaleはドキュメントに反しUTF-8を扱えない。mingw-w64でロケール名に与える文字コードはコードページ(整数)で指定するが利用できるものは限られる。日本語を表現できるのは932(シフトJIS)と20932(EUC-JP)だけで、20290(EBCDIC日本語)はsetlocaleできるが文字コード変換でエラー(例外送出)となり、その他はsetlocaleに失敗する。不明の理由でsetlocaleが機能しなくなる場合があり、例えば20290を設定すると以降のsetlocaleは全て失敗する。
サンプルコードでmingw-w64は日本語文字を出力せず文字コード不一致による文字化けは確認できない。msys2サブシステムGCCはPOSIX互換ターミナルで文字コード不一致による文字化けが確認できるが、コマンドプロンプトではなぜか文字化けしない。
C++ロケールについて、mingw-w64実装もPOSIX依存実装も共通コードを用いてPOSIXに準拠する。C言語ロケールについて、mingw-w64はmsvcrt.dllを利用してマイクロソフトに準拠し、POSIX依存はGNUランタイムglibcを利用してPOSIXに準拠する。つまりmingw-w64はC++ロケールとC言語ロケールの関係に矛盾がある。最も顕著な例はロケール名に空文字列""を与えて取得するネイティブ環境の定義で、マイクロソフト準拠はシステムロケールでスタートメニュー[設定|時刻と言語|地域|日付、時刻、地域の追加設定|地域|管理|システムロケールの変更]に従い、POSIX準拠はLANG環境変数に従う。mingw-w64はこれを解決できず、GCCウィンドウズ実装一般の問題として名前付きlocale構築を制限しているものと想像する。
ロケール名に空文字列""を与える場合の挙動をまとめる。
実装 | 名前付きlocale構築(C++) | setlocale(C言語) |
---|---|---|
mingw-w64 | LANGが未定義、"C"、"POSIX" の場合は"C"ロケール それ以外はエラー | システムロケール(例えばjapanese_japan.932) |
POSIX依存 | LANG(例えばja_JP.UTF-8) |
本サイトはC++標準ライブラリのロケール変更に強い動機を持たないが、codecvtファセットは例外として文字コード変換に重用する。ロケールの主たる機能は文字列レベルのデータ操作である一方で、codecvtのみ文字コードという異なるレベルのデータ操作を行う。参考文献をサイト作成者訳で引用する(Corentin Jabot, Locales, Encodings and Unicode, 2020, JTC1/SC22/WG21 P2020R0, pp.1-2)。
ロケールと文字コードは別の概念だが歴史的な理由でPOSIX以降のC言語/C++は両者を統合したモデルを採用してきた。そのためC言語/C++のロケール変更は文字コードに影響し、一方で文字コードはインターフェースに直接表れない。もちろんこれはその時点で標準化されていた文字コードが全世界全ての文字を表現できず、ターゲットとする言語や地域で使うごく一部の文字のみを表現していたためである。しかしながら地域固有の文字など存在せず、例えば日本語をターゲットとする文章で「"locale"の韓国語は로케일である」と記述するのに何の問題があろうか。C++の文脈においてロケールとはターゲットとする地域、言語あるいは文化において、情報を文字化して表示するための規則と理解するべきである。
レガシーな文字コードで記述した古いファイルはハードディスクの肥やしに残り、開発者が更新を放棄した有用なアプリケーションはユニコードを扱えず、ユニコードにしても複数のフォーマットが存在する。文字コード変換の需要は今でも確実に存在するが、本サイトはcodecvtをそういった文字コード変換のフレームワークとして活用する。C++標準ライブラリが供給するcodecvtの扱う文字コードは限定される上に実装依存が大きいため、任意の文字コードを扱えるcodecvtを文字コード変換ライブラリを利用して自作してしまう。
ソースコード(自作のcodecvtファセット)でiconvライブラリによるcodecvtを実装したが、本記事は実装方法をさらに詳解してICUライブラリによる実装とウィンドウズAPIによる実装を追加する。ウィンドウズアプリケーションは主にユニコード(手法)を用いるため内部文字コードはUTF-16として、議論のcodecvtは内部文字コードUTF-16、外部文字コード任意(文字コード変換ライブラリの扱えるもの)に固定する。この変換はN:Mでファイルストリームバッファの規格外(N4659 25.4.1.4.2/p3および脚注236)となるため、mingw-w64実装における安全性についても検討する。
本サイトはライブラリ利用方法に二つのスキームを提案する。
ソースコードはiconvライブラリ、ICUライブラリ、ウィンドウズAPIによる実装を比較し、二つのスキームのメリットデメリットを検討する。詳細はソースコードに譲り結論のみ記述する。
これらの実装は双方向ファイルストリームとしてはならず、ストリーム途中でロケールを変更してはならない。多くの場合に問題とはならないが一部に不可能な条件が存在し、安全を見越してできない物と考えた方が良い。ウィンドウズAPI実装はステートフルな文字コードを扱えない。速度についてウィンドウズAPI実装は文字コード変換関数の機能制限で大きく劣り、iconvライブラリ実装とICUライブラリ実装の優劣はつけ難い。
ライブラリ利用スキーム1のメリットは、codecvtインスタンスを複数のファイルストリームで安全に共有できる。デメリットの第一は、codecvtインスタンス構築がデフォルトに限定され、コンストラクタへ引数を渡す手段が無く使用する文字コードはコンパイル時に決定されて実行時の動的設定ができない。デメリットの第二は、ファイルストリーム(basic_fstream)を含む様々なクラステンプレートの特殊化が必要になる。これは暗黙的特殊化で十分で単に実体化すれば良い。文字列(basic_string)さえも特殊化が必要で、ファイルストリームの特殊化からwstringへシフト演算子による入出力ができず、文字列の特殊化を必要とする。文字列の特殊化とwstringの相互変換はc_strメンバ関数を必要とする。シフト演算子による書式化にはいくつかのファセットの特殊化をlocaleインスタンスに追加しなければならない。
ライブラリ利用スキーム2のメリットの第一は、codecvtインスタンス構築に引数を渡す事ができる(例示したソースコードはそのように書かれていないが)ので、使用する文字コードを動的設定できる。メリットの第二は、ファイルストリーム利用でシフト演算子による入出力が普通にできる。デメリットは大きく、codecvtインスタンスを複数のファイルストリームで共有できずimbueしたlocaleインスタンスをコピーしてはならない。
規格はファイルストリームバッファには必ず1:N変換のcovecvtファセットを使うとする(N4659 25.4.1.4.2/p3および脚注236)。しかしウィンドウズユニコード(手法)アプリケーションは内部文字コードUTF-16で、基本多言語面外の文字は2符号長を必要としてN:M変換とならざるを得ない。mingw-w64のbasic_filebufソースコードを調査して、N:M変換codecvtファセット利用の安全性を確認する。詳細はソースコードに譲り結論のみ記述する。
ファイル読み込みは外部文字コードを可変長変換とすれば問題なく行える。UTF-32などの固定長でも可変長として扱う必要がある。ファイル書き込みは後述する部分変換スキーム2の場合に内部バッファ終端が部分符号シーケンス(例えば最後がUTF-16の2符号長文字第一符号)となるとエラーとなる。これを解消するにはbasic_filebuf::overflowメンバ関数を修正しなければならない。basic_filebuf::sputbackcメンバ関数あるいはbasic_filebuf::sungetcメンバ関数で読み込んだ符号を入力ストリームに戻す場合、内部バッファの前に戻す事はできない。これはN:M変換の問題ではなく可変長変換として扱うためであり、すなわち1:N変換でも可変長(例えばUCS-2/UTF-8変換)なら同じ問題を抱える。
結論として、N:M変換codecvtファセットのファイル読み込みは安全。ファイル書き込みは部分変換スキーム1なら安全で、部分変換スキーム2はbasic_filebuf::overflowの修正が必要。符号を入力ストリームに戻す操作はしない方が良い。
ソースコード(自作のcodecvtファセット)で三つの疑問点を提示した。本記事の考察をまとめて、以下に解答する。
規格に合致するだけでなく、そのようにするべきである。つまり、部分変換スキーム1より部分変換スキーム2を採る方が良い。ただしN:M変換の場合は内部バッファ終端が部分符号シーケンスとなる場合に問題を抱える。例えばデフォルトならフラッシュせず4096番目にUTF-16の2符号長文字第一符号を出力するとエラーとなる。これを解消するにはbasic_filebuf::overflowを修正する必要がある。
教科書は部分変換スキーム1(Josuttis)と部分変換スキーム2(Langer et al.)で対立するが、ウェブ情報のほとんどが部分変換スキーム1を前提とする。JosuttisがKindle化した一方でLanger et al.が絶版なのがその理由かもしれない。部分変換スキーム1は部分符号シーケンスをstateに記憶する一般解が存在しないが、部分変換スキーム2はその必要が無い。部分変換スキーム2はバッファサイズが符号長より小さいと無限ループに陥るが、そのようなバッファはそもそも実用的でない。これは想像だが元々はステートフル前提で、部分変換スキーム2としてstateはエスケープシーケンスの選択情報のみを記憶する(従ってintで十分)としていたのではないだろうか。なおスキーム2の実装でも利用するライブラリが自ら部分符号シーケンスを記憶すればスキーム1として機能し、ICUはそういったライブラリである。
規格に合致しないと思われ、少なくとも規格は想定していない。しかしファイルストリームからの利用でコンストラクタに引数を渡して文字コードを実行時に動的設定するにはこれを用いざるを得ない。その場合、このcodecvtファセットを所有するlocaleインスタンスをコピーしてはならない。
これはC++規格ロケールオブジェクト(localeクラス)の今となっては設計ミスとしか思えない。百歩譲っても設計に文字コードの動的設定が考慮されていないと言うべきだろう。
ファイルストリームからの利用は規格に合致しないが、ウィンドウズユニコード(手法)アプリケーションはこれを用いざるを得ない。既述した部分変換スキーム2を採る場合の問題に加えて、このcodecvtファセットを所有するファイルストリームから読み込んだ文字(より正確には符号)をバッファに戻す操作を行ってはならない。
バッファに戻す操作については1:Nであっても可変長文字コードの場合に同じ制限を持ち、この制限は規格に合致しない事が理由ではない。いずれにせよウィンドウズの内部文字コードがUTF-16である限り、我慢しなければならない事項の一つであり続ける。UTF-32とchar32_tの美しい世界は恐らく到来しない。