ファイルストリームバッファにN:M変換となるcodecvtファセットを用いる場合の安全性を確認する。
規格はファイルストリームバッファには必ず1:N変換のcovecvtファセットを使うとする(JTC1/SC22/WG21 N4659 25.4.1.4.2/p3および脚注236)。しかしウィンドウズのユニコード(手法)アプリケーションは内部文字コードUTF-16で、基本多言語面外の文字は2符号長を必要としてN:M変換とならざるを得ない。mingw-w64のbasic_filebufクラステンプレートのソースコードを調査して、N:M変換cocecvtファセット利用の安全性を確認する。これはほぼヘッダオンリーライブラリで供給されソースコードを直接見ることができる。本記事はmingw-w64のバージョン11.2.0-5で検討し、各ファイルはC:\msys64\mingw32\include\c++\11.2.0\bitsあるいはC:\msys64\mingw64\include\c++\11.2.0\bitsに置かれる。
basic_filebufクラステンプレート
ソースコード(自作のcodecvtファセット)で定義したライブラリ利用スキーム1とスキーム2の利用するクラステンプレート特殊化は異なるが、basic_filebufはプライマリテンプレートのみを供給し(N4659 30.9.2)ソースコードは共用する。
利用元 | 特殊化 |
ライブラリ利用スキーム1 | basic_filebuf<wchar_t,TMyTraits> |
ライブラリ利用スキーム2 | wfilebuf=basic_filebuf<wchar_t,char_traits<wchar_t> |
以降、basic_filebufクラステンプレートの特殊化を単にbasic_filebufクラス、それから構築されたインスタンスをbasic_filebufインスタンスと参照する。basic_filebufクラステンプレートはテンプレート仮引数を同じくするbasic_streambufクラステンプレート(30.6.3)を継承し、その特殊化をbasic_streambufクラスとして参照する。以下に本記事で説明するbasic_filebufクラスメンバ関数をまとめるが、これらはbasic_streambufクラスの仮想プロテクテッドメンバ関数をオーバーライドする。
メンバ関数 | 概要 | 規格(N4659) | ソースコード |
underflow | ファイルからの読み込み | 30.9.2.4/p3 | fstream.tcc:321 |
overflow | ファイルへの書き込み | 30.9.2.4/p10-11 | fstream.tcc:539 |
pbackfail | 入力シーケンスに符号を戻す時の特別な処理 | 30.9.2.4/p5-9 | fstream.tcc:480 |
basic_filebufクラスは入出力シーケンスをファイルに関連付ける(30.9.2)。入出力シーケンスは内部符号列に保持し(内部バッファ)、ファイルとは外部符号列(外部バッファ)を介してデータ交換する。内部バッファと外部バッファはcodecvtファセットで相互変換する。mingw-w64は内部バッファを入出力で一つのメンバ変数に共用する。外部バッファは入力をメンバ変数に保持する一方、出力はメンバ関数ローカル変数に保持する。ファイルからの読み込み(underflow)は最初に外部バッファへ外部符号列を読み込み、codecvtファセットで変換して内部バッファへ書き込む。ファイルへの書き込み(overflow)は最初に内部バッファ符号列を変換して外部バッファに書き込み、外部バッファ符号列をファイルへ書き込む。入力シーケンスは読み込んだ符号をシーケンスに戻せるが、内部バッファ先頭からさらに戻すなどの場合に特別な処理(pbackfail)を必要とする。これらの関数をコールするパブリックメンバ関数(sgetc、sputc、sungetcなど)の説明は省略する。本記事はmingw-w64のunderflow、overflow、pbackfailの実装をソースコードから解析する。
説明に用いるソースコードは簡略化してオリジナルとは異なる。ソースコードに現れるbasic_filebufメンバ変数をまとめる。テンプレート仮引数の_CharTと_Tratisはクラススコープでchar_typeとtraits_typeにtypedefされる。本記事で_CharTの実引数はwchar_tで、_Traitsの実引数デフォルトはchar_traits<wchar_t>で、_Traits::state_typeはmbstate_tである。なおcodecvtクラステンプレートの特殊化(ライブラリ利用スキーム1)は_Traitsのデフォルトを用いない。
メンバ変数 | 型 | 説明 |
_M_buf | _CharT* | 内部バッファ先頭ポインタ |
_M_buf_size | size_t | 内部バッファサイズ、最大内部符号数+1(オーバーフロー符号領域対応) |
_M_reading | bool | 読み込み中フラグ |
_M_writing | bool | 書き込み中フラグ |
_M_codecvt | (説明参照) | codecvt<_CharT,char,_Traits::state_type>ファセットへのポインタ |
_M_file | (説明参照) | ファイル、独自実装の__basic_file<char>型でほぼfilebuf同等 |
_M_ext_buf | char* | 外部バッファ先頭ポインタ、eback()に対応 |
_M_ext_buf_size | streamsize | 外部バッファサイズ |
_M_ext_next | const char* | 外部バッファに読み込まれたが変換されていない符号列(残余外部符号列)の先頭ポインタ、egptr()に対応 |
_M_ext_end | char* | 外部バッファ読み込み終端ポインタ |
_M_state_cur | _Traits::state_type | pptr()/egptr()に対応するcodecvtの状態 |
_M_state_last | _Traits::state_type | eback()に対応するcodecvtの状態 |
内部バッファのポインタはパブリックメンバ関数で取得、設定する。
分類 | メンバ関数 | 説明 | 規格(N4659) |
入力 | 取得 | _CharT* eback() const | 入力シーケンス(内部バッファ)先頭ポインタ | 30.6.3.3.2/p1 |
_CharT* gptr() const | 入力シーケンスから読み出す次のポインタ | 30.6.3.3.2/p2 |
_CharT* egptr() const | 入力シーケンス終端ポインタ | 30.6.3.3.2/p3 |
設定 | void gbump(int n) | gptr()をnだけ進める | 30.6.3.3.2/p4 |
void setg(_CharT* beg, _CharT* next, _CharT* end) | eback()をbeg、gptr()をnext、egptr()をendに設定 | 30.6.3.3.2/p5 |
出力 | 取得 | _CharT* pbase() const | 出力シーケンス(内部バッファ)先頭ポインタ | 30.6.3.3.3/p1 |
_CharT* pptr() const | 出力シーケンスに書き込む次のポインタ | 30.6.3.3.3/p2 |
_CharT* epptr() const | 出力シーケンス終端ポインタ | 30.6.3.3.3/p3 |
設定 | void pbump(int n) | pptr()をnだけ進める | 30.6.3.3.3/p4 |
void setp(_CharT* beg, _CharT* end) | pbase()とpptr()をbeg、epptr()をendに設定 | 30.6.3.3.3/p5 |
ファイルからの読み込み(underflow)
ファイルからの読み込みはunderflow仮想プロテクテッドメンバ関数のオーバーライドが行う。
実装の説明
_M_ext_bufにファイルから読み込み、codecvt::in関数で_M_bufに変換出力する。詳細は以下のソースコード概略の理解に委ねるが、特に注意すべき点をまとめる。
- 内部バッファが保持できる符号数の最大値(最大内部符号数)は(内部バッファサイズ)-1とするが、これは後述のoverflowへの対応である。
- 外部バッファは_M_ext_bufに確保する。外部符号が固定長の場合と可変長(ステートフルを含む)の場合で外部バッファの扱いが異なる。
- 外部符号が固定長の場合は(外部バッファサイズ)=(最大内部符号数)×(固定符号長)とする。1:N変換が保証されれば内部/外部符号の対応は固定整数比となり、外部バッファ符号列はcodecvt::inで全て一括して内部バッファ符号列に変換する。外部符号数が十分であれば内部バッファは最大内部符号数で満たされる。
- 外部符号が可変長の場合は(外部バッファサイズ)=(最大内部符号数)+(最大符号長-1)とする。まず外部バッファ符号列の最初の最大内部符号数だけをcodecvt::in変換する。変換された内部バッファ符号列は最大に見積もっても最大内部符号数なので内部バッファはこれを収容し、このためcodecvt::inがpartialを返すのは外部符号列が部分符号シーケンスで終わる場合に限られる。部分符号シーケンスで終わる場合は外部バッファ符号列の残りから1符号ずつを追加して変換成功までcodecvt::inを繰り返し、最大で(最大符号長-1)個の符号を追加する。
- 最大内部符号数は変換符号列を収めるに十分な大きさであるが、外部符号可変長の場合には部分符号シーケンスが外部バッファに残る場合がある。いずれにせよ外部バッファに残る外部符号列(残余外部符号列)は外部バッファ先頭へ移動し、次回のunderflowコールに備える。
basic_filebuf::underflowメンバ関数
template<typename _CharT, typename _Traits>
typename basic_filebuf<_CharT, _Traits>::int_type
basic_filebuf<_CharT, _Traits>::underflow()
{
int_type __ret = traits_type::eof();
const bool __testin = _M_mode & ios_base::in;
if (__testin)
{
if (_M_writing) {...}
_M_destroy_pback();
if (this->gptr() < this->egptr()) {return ...}
const size_t __buflen = _M_buf_size > 1 ? _M_buf_size - 1 : 1;
bool __got_eof = false;
streamsize __ilen = 0;
codecvt_base::result __r = codecvt_base::ok;
if (__check_facet(_M_codecvt).always_noconv()) {...}
else
{
const int __enc = _M_codecvt->encoding();
streamsize __blen;
streamsize __rlen;
if (__enc > 0) __blen = __rlen = __buflen * __enc;
else
{
__blen = __buflen + _M_codecvt->max_length() - 1;
__rlen = __buflen;
}
const streamsize __remainder = _M_ext_end - _M_ext_next;
__rlen = __rlen > __remainder ? __rlen - __remainder : 0;
if (_M_reading && this->egptr() == this->eback() && __remainder)
__rlen = 0;
if (_M_ext_buf_size < __blen)
{
...
if (__remainder) builtin_memcpy(__buf, _M_ext_next, __remainder);
...
}
else if (__remainder) __builtin_memmove(_M_ext_buf, _M_ext_next, __remainder);
_M_ext_next = _M_ext_buf;
_M_ext_end = _M_ext_buf + __remainder;
_M_state_last = _M_state_cur;
do
{
if (__rlen > 0)
{
if (_M_ext_end - _M_ext_buf + __rlen > _M_ext_buf_size) {...}
streamsize __elen = _M_file.xsgetn(_M_ext_end, __rlen);
if (__elen == 0) __got_eof = true;
else if (__elen == -1) break;
_M_ext_end += __elen;
}
char_type* __iend = this->eback();
if (_M_ext_next < _M_ext_end)
__r = _M_codecvt->in(_M_state_cur,
_M_ext_next, _M_ext_end, _M_ext_next,
this->eback(), this->eback() + __buflen, __iend);
if (__r == codecvt_base::noconv) {...}
else __ilen = __iend - this->eback();
if (__r == codecvt_base::error) break;
__rlen = 1;
}
while (__ilen == 0 && !__got_eof);
}
if (__ilen > 0)
{
_M_set_buffer(__ilen);
_M_reading = true;
__ret = traits_type::to_int_type(*this->gptr());
}
else if (__got_eof)
{
_M_set_buffer(-1);
_M_reading = false;
if (__r == codecvt_base::partial) {...}
}
else if (__r == codecvt_base::error) {...}
else {...}
}
return __ret;
}
N:M安全性の検討
この実装をN:M変換で利用する事は、はたして安全であろうか。外部符号が可変長の場合、1:N変換を仮定する箇所は無く安全である。外部符号が固定長の場合、内部/外部符号が固定整数比とならずエラーをもたらす。内部/外部文字コードがUTF-16/UTF-32で例示しよう。UTF-32は1符号(32ビット)/1文字であるがファイル入出力はバイトストリームなので4符号(8ビット×4)/1文字の固定長として扱う。変換結果としてUTF-16の2符号長文字を内部バッファに出力すると、内部バッファは全てを収容できず残余外部符号列が発生する。次のunderflowコールは残余外部符号列を無視して(最大内部符号数)×(固定符号長)の外部符号列を読み込もうとして、外部バッファオーバーフローが発生する。ただし実際は外部バッファサイズの不正チェックに引っかかり例外発生でエラーとなる。
この解決は簡単で、外部符号が固定長であろうが可変長として扱ってしまえば良い。内部文字コードがUTF-16である限り内部/外部符号が固定整数比となる事はありえず当然の帰結だろう。結論として全ての外部文字コードを可変長として扱えば、N:M変換でもファイル読み込みは安全である。
TMyCodeCvtStateXXXのスタティックメンバ関数
外部文字コードを可変長として扱わせるにはcodecvt::do_encoding()が0を返せば良い。本サイトはソースコード(codecvt実装方法の詳解)でiconvライブラリ利用、ICUライブラリ利用、ウインドウズAPI利用のcodecvtファセットを提案するが、これら全て実装をTMyCodeCvtStateIconv、TMyCodeCvtStateICUあるいはTMyCodeCvtStateWinAPIといったクラステンプレート(TMyCodeCvtStateXXXクラステンプレート)の特殊化に移譲し、そのメンバ関数で対応する。これらの関数は定数を返すスタティックメンバ関数とした。
do_always_noconv()は当然false。do_encoding()は上記理由で外部文字コード固定長だろうが可変長だろうが0。ステートフルとしても実装に可変長との差は無く0で構わない。do_max_length()はケチらず、全ての文字コードにおける最大符号長のMB_LEN_MAXに固定する。mingw-w64実装のMB_LEN_MAXは5で、これは例えばJIS(ISO-2022-JP)のエスケープシーケンス3符号+2符号長文字に対応できる。do_max_length()は外部バッファサイズの計算に用いられる。入力時外部バッファサイズは(最大内部符号数)+(最大符号長-1)なのでdo_max_length()を過大に与えてもインパクトは無視できる。出力時の詳細は後述するが一時的に確保される外部バッファサイズは(内部符号数)×(最大符号長)で、実装のデフォルトは内部バッファ符号数4096なので最大20KiBであり、これも問題となる数字ではない。どうしても気になるならdo_max_length()を与えられた外部文字コードの最大符号長で返せば良い。
- 覚え書き
- 最大符号長5を超える文字コードはサイト作成者の知る限りCESU-8の最大6符号がある。リンク先によればISO-2022-KRが最大7符号だそうだ。ISO/IEC 2022のウィキペディアをナナメ読みすればエスケープシーケンスだけで4符号を消費するものが多数見つかる。リナックスは十分な余裕を取ってMB_LEN_MAXは16だそうだ。do_max_length()を5に決め打ちするのは相当に心配だが、これはMB_LEN_MAXを5とした実装側の責任だ。
スタティックメンバ関数 | 説明 | 戻り値 |
do_always_noconv | 常に無変換ならtrue | false |
do_max_length | 外部文字コードの最大符号長 | MB_LEN_MAX |
do_encoding | >0:固定符号長,0:可変符号長,-1:ステートフル | 0 |
TMyCodeCvtStateXXXクラステンプレート
template<typename T,typename U> class TMyCodeCvtStateXXX
{
....
public:
....
static bool do_always_noconv() noexcept {return false;}
static int do_max_length() noexcept {return MB_LEN_MAX;}
static int do_encoding() noexcept {return 0;}
};
ファイルへの書き込み(overflow)
ファイルへの書き込みはoverflow仮想プロテクテッドメンバ関数のオーバーライドが行う。
実装の説明
overflowオーバーライドは仮想でない_M_convert_to_externalプロテクテッドメンバ関数で変換とファイル書き込みを行う。_M_convert_to_externalは一時領域に外部バッファ(__buf)を生成し、内部バッファ(_M_buf)からcodecvt::out関数で変換出力する。詳細は以下のソースコード概略の理解に委ねるが、特に注意すべき点をまとめる。
- epptr()は_M_buf+_M_buf_size-1に設定し、すなわち内部バッファが保持できる符号数の最大値(最大内部符号数)は(内部バッファサイズ)-1とする。これは__cがtraits_type::eof()(EOF)でない場合の追加スペースを確保するためである(オーバーフロー符号領域)。
- 外部バッファは一時領域に確保する(__buf)。GCC拡張__builtin_alloca(GCC 14.1.0 Manual 6.61 Other Built-in Functions Provided by GCC)で確保し、スコープを抜けると自動開放される。
- 外部バッファサイズは(内部符号数)×(最大符号長)とする。外部バッファは内部バッファ符号列の変換を収容するに十分な大きさで、1:N変換が保証されればcodecvt::outはpartialを返さない。従ってunderflowと異なり内部バッファに内部符号列が残る(残余内部符号列)事は無く、次回のoverflowコールに備え内部バッファ先頭へコピーする処理も行わない。
- 実装はpartialを返した場合に"一度だけ"残余内部符号列の変換を試みるが、上記の理由で実効しない。その存在理由は良く解からないが規格対応(N4659 30.9.2.4/p10.3)と想像する。
- 覚え書き
- 該当規格(N4659 30.9.2.4/p10.3)を示す。"repeat"が"一度だけ"を含意するか微妙だが、そもそも実効しないはずなので深追いしない。
If r == codecvt_base::partial then output to the file characters from xbuf up to xbuf_end, and repeat using characters from end to p. If output fails, fail (without repeating).
basic_filebuf::overflowメンバ関数
template<typename _CharT, typename _Traits>
typename basic_filebuf<_CharT, _Traits>::int_type
basic_filebuf<_CharT, _Traits>::overflow(int_type __c)
{
int_type __ret = traits_type::eof();
const bool __testeof = traits_type::eq_int_type(__c, __ret);
const bool __testout = (_M_mode & ios_base::out || _M_mode & ios_base::app);
if (__testout)
{
if (_M_reading) {...}
if (this->pbase() < this->pptr())
{
if (!__testeof) {...}
if (_M_convert_to_external(this->pbase(), this->pptr() - this->pbase()))
{
_M_set_buffer(0);
__ret = traits_type::not_eof(__c);
}
}
else if (_M_buf_size > 1)
{
_M_set_buffer(0);
_M_writing = true;
if (!__testeof) {...}
__ret = traits_type::not_eof(__c);
}
else {...}
}
return __ret;
}
template<typename _CharT, typename _Traits>
bool
basic_filebuf<_CharT, _Traits>::_M_convert_to_external
(_CharT* __ibuf, streamsize __ilen)
{
streamsize __elen;
streamsize __plen;
if (__check_facet(_M_codecvt).always_noconv()) {...}
else
{
streamsize __blen = __ilen * _M_codecvt->max_length();
char* __buf = static_cast<char*>(__builtin_alloca(__blen));
char* __bend;
const char_type* __iend;
codecvt_base::result __r;
__r = _M_codecvt->out(_M_state_cur,
__ibuf, __ibuf + __ilen, __iend,
__buf, __buf + __blen, __bend);
if (__r == codecvt_base::ok || __r == codecvt_base::partial)
__blen = __bend - __buf;
else if (__r == codecvt_base::noconv) {...}
else {...}
__elen = _M_file.xsputn(__buf, __blen);
__plen = __blen;
if (__r == codecvt_base::partial && __elen == __plen)
{...}
}
return __elen == __plen;
}
N:M安全性の検討
この実装をN:M変換で利用する事は、はたして安全であろうか。N:M変換の要求する外部バッファサイズは最大でも1:N変換に等しくcodecvt::outがバッファ不足でpartialを返すことは無いが、内部バッファ符号列が部分符号シーケンスで終わるかもしれない。その場合もcodecvt::outはpartialを返すがoverflowは対応せず残余内部符号列として残る部分符号シーケンスを捨てる。以降、部分符号シーケンスの後続から内部バッファ先頭に書き込まれ、次回overflowコールはその変換を試みてエラーとなる。実用例で言えば、内部コードUTF-16で内部バッファサイズデフォルトが4096符号長なので、UTF-16符号列がフラッシュせず出力シーケンスに書き込まれ続け4096番目が2符号長文字の第一符号である場合にエラーとなる。そのような条件は普通なら稀なので発見しづらいバグと言えよう。
部分符号シーケンスが残るかどうかはcodecvtがソースコード(自作のcodecvtファセット)に定義する部分変換の実装方法に依存する。
- from_nextをfrom_endへ移動し、部分符号シーケンスを記憶(例えば2符号長文字の第一符号をstateに代入)し、partialを返す(部分変換スキーム1)。
- from_nextを部分符号シーケンスの先頭へ移動し、partialを返す(部分変換スキーム2)。
部分変換スキーム1は部分符号シーケンスを内部バッファに残さないので問題とならず、部分変換スキーム2のみで問題となる。本サイトは部分符号シーケンスの扱いを省略できる部分変換スキーム2の方が優れると主張してきたため、これはかなり困った事だ。ところでICUライブラリだけは生来の機能として部分符号シーケンスを変換状態に記憶する。本サイトはcodecvtとしてiconvライブラリ利用、ICUライブラリ利用、ウィンドウズAPI利用の三方法を提案するが、従ってICUライブラリ利用だけは部分変換スキーム1でこの問題とは無関係である。
この問題を再現するテストコードを提示する。iconvライブラリ利用codecvtは内部バッファサイズに依存してエラーが発生したりしなかったりする。ICUライブラリ利用は常にエラーが発生しない。部分変換スキーム2でエラーを発生させないためには、overflowを修正して残余内部符号列を内部バッファ先頭にコピーして次のoverflowコールに備えれば良い。
テストコード
問題を容易に再現させるため、basic_filebufの内部バッファサイズをpubsetbufメンバ関数で小さい値(0~9)に変更している。バッファサイズ0とするとpubsetbufに失敗してデフォルトの内部バッファが用いられてエラーは発生しない。iconvライブラリ利用のcodecvtは内部バッファサイズに依存してエラーが発生する。ICUライブラリ利用のcodecvtは常にエラーを発生しない。
#include <iostream>
#include <fstream>
#include "TMyCodeCvt.h"
#include "TMyCodeCvtExt.h"
void WriteToFile(size_t bufSize,const wchar_t* text,const char* fileName)
{
struct Buf
{
wchar_t* ptr_;
Buf(size_t bufSize):ptr_{new wchar_t[bufSize]} {}
~Buf() {delete[] ptr_;}
} buf{bufSize};
auto loc=std::locale{std::locale{}
,new TMyCodeCvt<TMyCodeCvtStateIconv<NMyEncoding::UTF16,NMyEncoding::UTF8>>{}};
auto fOut=std::wofstream{};
fOut.imbue(loc);
fOut.rdbuf()->pubsetbuf(buf.ptr_,bufSize);
fOut.open(fileName);
fOut<<text<<std::flush;
std::cout<<fileName<<" is "<<(fOut?"Good.":"Bad.")<<std::endl;
}
void DoTest(size_t bufSize)
{
std::cout<<"Buffer size="<<bufSize<<std::endl;
WriteToFile(bufSize,L"😀😁😂😃","FourEmoticons.txt");
WriteToFile(bufSize,L"あ😀😁😂😃","OneHiragana_FourEmoticons.txt");
}
int main()
{
for (auto bufSize=0;bufSize<10;++bufSize) {DoTest(bufSize);}
return 0;
}
overflowの修正
overflowを修正して、部分符号シーケンスが残る場合に残余内部符号列を内部バッファ先頭にコピーして次のoverflowコールに備える。basic_filebufを直接書き換えるのはさすがに控えるべきで、派生のTMyFileBufクラステンプレートを作成してoverflowをオーバーライドする。_M_convert_to_externalメンバ関数も修正するが仮想メンバ関数ではなく名前隠蔽になる。
overflow、_M_convert_to_external共にbasic_filebufとほぼ変わらずコピーして一部を変更した。変更点をまとめる。
- _M_convert_to_externalに三番目の仮引数として参照型__iendを追加して、codecvt::outで変換された内部バッファ終端を呼出し元に返す。
- overflowはローカル変数__iendを三番目の実引数として_M_convert_to_externalをコールする。
- _M_convert_to_externalのコールに成功すると、__iendを用いて残余内部符号列サイズを計算して内部バッファ先頭へ移動する。
- 残余内部符号列サイズからpptr()をpbump()で適切に移動する。
- Unbufferedは説明していないが外部バッファを経由せず外部符号一つずつ直接ファイル出力するモードである。N:M変換は不可能な場合があるので無条件で例外送出する。
TMyFileBufクラステンプレート(TMyFileBuf.h)
basic_filebufのメンバは関数、変数共にほとんどpublic/protectedなのでオーバーライドは容易である。しかし基底クラスがテンプレート仮引数に依存するためメンバアクセスはpublic/protectedであっても基底クラス名で修飾するか(例えばstd::basic_filebuf<_CharT,_Traits>::_M_file)、あるいはusingでクラススコープに導入する。TMyFileBufはstd名前空間に属さずstd名前空間に属する名前はstd修飾するか、あるいはusing namespaceでブロックスコープに導入する。コピー元となるべくコード共通化するためusingとusing namespaceで対処した。
#ifndef TMYFILEBUF_H_INCLUDED
#define TMYFILEBUF_H_INCLUDED
template<typename _CharT=wchar_t,typename _Traits=std::char_traits<_CharT>>
class TMyFileBuf:public std::basic_filebuf<_CharT,_Traits>
{
private:
using base_class=std::basic_filebuf<_CharT,_Traits>;
public:
using typename base_class::char_type;
using typename base_class::traits_type;
using typename base_class::int_type;
using typename base_class::pos_type;
using typename base_class::off_type;
protected:
using base_class::_M_file;
using base_class::_M_mode;
using base_class::_M_state_cur;
using base_class::_M_state_last;
using base_class::_M_buf_size;
using base_class::_M_reading;
using base_class::_M_writing;
using base_class::_M_codecvt;
using base_class::_M_destroy_pback;
using base_class::_M_seek;
using base_class::_M_get_ext_pos;
using base_class::_M_set_buffer;
protected:
int_type overflow(int_type ch=traits_type::eof()) override;
bool _M_convert_to_external(char_type*,std::streamsize,const char_type*&);
};
template<typename _CharT,typename _Traits>
typename TMyFileBuf<_CharT,_Traits>::int_type
TMyFileBuf<_CharT,_Traits>::overflow(int_type __c)
{
using namespace std;
int_type __ret = traits_type::eof();
const bool __testeof = traits_type::eq_int_type(__c, __ret);
const bool __testout = (_M_mode & ios_base::out || _M_mode & ios_base::app);
if (__testout)
{
if (_M_reading)
{
_M_destroy_pback();
const int __gptr_off = _M_get_ext_pos(_M_state_last);
if (_M_seek(__gptr_off, ios_base::cur, _M_state_last) == pos_type(off_type(-1)))
return __ret;
}
if (this->pbase() < this->pptr())
{
if (!__testeof)
{
*this->pptr() = traits_type::to_char_type(__c);
this->pbump(1);
}
const char_type* __iend;
if (_M_convert_to_external(this->pbase(), this->pptr() - this->pbase(), __iend))
{
auto unconverted = this->pptr() - __iend;
_M_set_buffer(0);
memcpy(this->pbase(), __iend, unconverted*sizeof(char_type));
this->pbump(unconverted);
__ret = traits_type::not_eof(__c);
}
}
else if (_M_buf_size > 1)
{
_M_set_buffer(0);
_M_writing = true;
if (!__testeof)
{
*this->pptr() = traits_type::to_char_type(__c);
this->pbump(1);
}
__ret = traits_type::not_eof(__c);
}
else
{
__throw_ios_failure(__N("TMyFileBuf::overflow cannot work with unbuffered"));
}
}
return __ret;
}
template<typename _CharT,typename _Traits>
bool
TMyFileBuf<_CharT,_Traits>::_M_convert_to_external
(char_type* __ibuf, std::streamsize __ilen,const char_type*& __iend)
{
using namespace std;
streamsize __elen;
streamsize __plen;
if (__check_facet(_M_codecvt).always_noconv())
{
__elen = _M_file.xsputn(reinterpret_cast<char*>(__ibuf), __ilen);
__plen = __ilen;
}
else
{
streamsize __blen = __ilen * _M_codecvt->max_length();
char* __buf = static_cast<char*>(__builtin_alloca(__blen));
char* __bend;
codecvt_base::result __r;
__r = _M_codecvt->out(_M_state_cur, __ibuf, __ibuf + __ilen,
__iend, __buf, __buf + __blen, __bend);
if (__r == codecvt_base::ok || __r == codecvt_base::partial)
__blen = __bend - __buf;
else if (__r == codecvt_base::noconv)
{
__buf = reinterpret_cast<char*>(__ibuf);
__blen = __ilen;
}
else
__throw_ios_failure(__N("basic_filebuf::_M_convert_to_external "
"conversion error"));
__elen = _M_file.xsputn(__buf, __blen);
__plen = __blen;
if (__r == codecvt_base::partial && __elen == __plen)
{
const char_type* __iresume = __iend;
streamsize __rlen = this->pptr() - __iend;
__r = _M_codecvt->out(_M_state_cur, __iresume,
__iresume + __rlen, __iend, __buf,
__buf + __blen, __bend);
if (__r != codecvt_base::error)
{
__rlen = __bend - __buf;
__elen = _M_file.xsputn(__buf, __rlen);
__plen = __rlen;
}
else
__throw_ios_failure(__N("basic_filebuf::_M_convert_to_external "
"conversion error"));
}
}
return __elen == __plen;
}
#endif
テストコード
ストリームバッファをTMyFileBufクラステンプレート特殊化に置き換えるため、wofstreamでなくwostreamを用いる。前者もwostream&にキャストすれば同等の事ができるが余分なストリームバッファを持つデメリットがある。iconvライブラリ利用codecvtでもバッファサイズ1を例外としてエラーを生じない。バッファサイズ0がエラーとならないのは前のテストコードで説明した。バッファサイズ1がエラーとなるのはUnbufferedモード同等となるためで、詳しくはbasic_filebuf::setbufソースコードのコメント参照の事。TMyFileBufはUnbufferedモードで無条件に例外送出する。
#include <iostream>
#include <fstream>
#include "TMyCodeCvt.h"
#include "TMyCodeCvtExt.h"
#include "TMyFileBuf.h"
void WriteToFile(size_t bufSize,const wchar_t* text,const char* fileName)
{
struct Buf
{
wchar_t* ptr_;
Buf(size_t bufSize):ptr_{new wchar_t[bufSize]} {}
~Buf() {delete[] ptr_;}
} buf{bufSize};
auto loc=std::locale{std::locale{}
,new TMyCodeCvt<TMyCodeCvtStateIconv<NMyEncoding::UTF16,NMyEncoding::UTF8>>{}};
auto fOutBuf=TMyFileBuf{};
fOutBuf.pubsetbuf(buf.ptr_,bufSize);
fOutBuf.open(fileName,std::ios_base::out);
auto fOut=std::wostream{&fOutBuf};
fOut.imbue(loc);
fOut<<text<<std::flush;
std::cout<<fileName<<" is "<<(fOut?"Good.":"Bad.")<<std::endl;
}
void DoTest(size_t bufSize)
{
std::cout<<"Buffer size="<<bufSize<<std::endl;
WriteToFile(bufSize,L"😀😁😂😃","FourEmoticons.txt");
WriteToFile(bufSize,L"あ😀😁😂😃","OneHiragana_FourEmoticons.txt");
}
int main()
{
for (auto bufSize=0;bufSize<10;++bufSize) {DoTest(bufSize);}
return 0;
}
入力シーケンスに符号を戻す時の特別な処理(pbackfail)
入力シーケンスに符号を戻す時の特別な処理はpbackfail仮想プロテクテッドメンバ関数のオーバーライドが行う。
実装の説明
basic_streambufのsputbackcパブリックメンバ関数(N4659 30.6.3.2.4/p1)とsungetcパブリックメンバ関数(30.6.3.2.4/p2)は読み込んだ符号を戻し、あるいは異なる符号を仮の入力シーケンスに戻す。これらはeback()==gptr()、あるいは戻す符号を__iとして__i!=*(gptr()-1)の場合にpbackfailをコールする。pbackfailは__iがEOFなら読み込んだ符号を戻し、そうでなければ__iを仮の入力シーケンスに戻す。詳細は以下のソースコード概略の理解に委ねるが、特に注意すべき点をまとめる。
- eback()<gptr()ならgptr()を一つ戻す。eback()==gptr()なら仮想プロテクテッドメンバ関数seekoffのオーバーライド(30.9.2.4/p13-15)でファイル位置を1内部符号分だけ戻してunderflowで内部バッファ符号列を読み直す。
- __iがEOFでなければpbackバッファ(仮の入力シーケンス)を作成して__iを書き込み、gptr()をpbackバッファに設定する。
basic_filebuf::pbackfailメンバ関数
pbackバッファを説明する。pbackバッファとは読み込んだ符号と異なる符号を仮の入力シーケンスに戻す時に保持する領域である。異なる符号を真でなく仮の入力シーケンスに戻す理由は規格要件による(30.9.2.4/p8)。pbackバッファ(仮の入力シーケンス)に符号を戻すとgptr()はpbackバッファを指す。ここからgptr()を移動するとpbackバッファは解体され内部バッファ(真の入力シーケンス)該当位置に戻る。実装のpbackバッファは最大で1内部符号を保持し、読み込んだ符号と異なる符号を二つ以上戻すとエラーとなる。
template<typename _CharT, typename _Traits>
typename basic_filebuf<_CharT, _Traits>::int_type
basic_filebuf<_CharT, _Traits>::pbackfail(int_type __i)
{
int_type __ret = traits_type::eof();
const bool __testin = _M_mode & ios_base::in;
if (__testin)
{
if (_M_writing) {...}
const bool __testpb = _M_pback_init;
const bool __testeof = traits_type::eq_int_type(__i, __ret);
int_type __tmp;
if (this->eback() < this->gptr())
{
this->gbump(-1);
__tmp = traits_type::to_int_type(*this->gptr());
}
else if (this->seekoff(-1, ios_base::cur) != pos_type(off_type(-1)))
{
__tmp = this->underflow();
if (traits_type::eq_int_type(__tmp, __ret)) return __ret;
}
else return __ret;
if (!__testeof && traits_type::eq_int_type(__i, __tmp)) __ret = __i;
else if (__testeof) __ret = traits_type::not_eof(__i);
else if (!__testpb)
{
_M_create_pback();
_M_reading = true;
*this->gptr() = traits_type::to_char_type(__i);
__ret = __i;
}
}
return __ret;
}
N:M安全性の検討
この実装をN:M変換で利用する事は、はたして安全であろうか。問題はseekoffに存在する。それはN:M変換ではなく可変符号長の問題であるものの、本サイトはN:M変換を必ず可変符号長として扱う。ファイル位置を移動するには内部符号列移動量から外部符号列移動量を計算しなければならない。これは内部/外部符号の対応が固定整数比となる固定符号長なら容易であるが、そうでない場合は困難で規格も失敗と定義する(30.9.2.4/p13)。seekoffに失敗すればpbackfailは失敗する。すなわちeback()==gptr()で読み込んだ符号あるいは異なる符号を戻そうとすると失敗する。
basic_filebuf::seekoffメンバ関数
template<typename _CharT, typename _Traits>
typename basic_filebuf<_CharT, _Traits>::pos_type
basic_filebuf<_CharT, _Traits>::seekoff
(off_type __off, ios_base::seekdir __way, ios_base::openmode)
{
int __width = 0;
if (_M_codecvt) __width = _M_codecvt->encoding();
if (__width < 0) __width = 0;
pos_type __ret = pos_type(off_type(-1));
const bool __testfail = __off != 0 && __width <= 0;
if (this->is_open() && !__testfail)
{
...
}
return __ret;
}
seekoff規格要件について
外部文字コード可変長あるいはステートフルでseekoffが失敗するのは規格要件(30.9.2.4/p13)なので順守して修正しない(そもそもN:M変換をファイルストリームバッファで使うのは規格外だが、そこは棚に上げて)。まとめれば本サイト提案のcodecvtを利用する場合、内部バッファ先頭で符号をsputbackcあるいはsungetcで入力ストリームに戻そうとすると失敗する。