パソコンでプログラミングしよう ウィンドウズC++プログラミング環境の構築
1.6.3.6(15)
ソースコード(N:M変換の安全性)

ファイルストリームバッファに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) // ファイルがios_base::inで開かれている場合
{
if (_M_writing) {...} // 書き込み中からコールされた場合は書き込み処理をリセット
_M_destroy_pback(); // pbackバッファがあれば解体
if (this->gptr() < this->egptr()) {return ...} // gptr()<egptr()なら*gptr()を返してリターン
const size_t __buflen = _M_buf_size > 1 ? _M_buf_size - 1 : 1; // 最大内部符号数=内部バッファサイズ-1
// M_buf_size==1はUnbuffered(説明省略)
bool __got_eof = false; // EOF(ファイル読み込み終了)フラグ
streamsize __ilen = 0; // 生成された内部符号数
codecvt_base::result __r = codecvt_base::ok; // codecvt::inの返値
if (__check_facet(_M_codecvt).always_noconv()) {...} // 常に無変換の場合
else // 変換の場合
{
const int __enc = _M_codecvt->encoding(); // 外部符号長、>0:固定、0:可変、-1:ステートフル(可変として扱う)
streamsize __blen; // 必要な外部バッファサイズ
streamsize __rlen; // 読み込む外部符号数
if (__enc > 0) __blen = __rlen = __buflen * __enc; // 外部符号固定長の場合の__blenと__rlen
else
{
__blen = __buflen + _M_codecvt->max_length() - 1; // 外部符号可変長の場合の__blen
__rlen = __buflen; // 外部符号可変長の場合の__rlen
}
const streamsize __remainder = _M_ext_end - _M_ext_next; // 残余外部符号数
__rlen = __rlen > __remainder ? __rlen - __remainder : 0; // __rlenを残余外部符号数だけ減らす
if (_M_reading && this->egptr() == this->eback() && __remainder)
__rlen = 0; // 他関数が_M_readingをセットした場合で残余外部符号あれば__rlenはゼロ
if (_M_ext_buf_size < __blen) // 外部バッファサイズ不足の場合
{
... // 新しい外部バッファを確保
if (__remainder) builtin_memcpy(__buf, _M_ext_next, __remainder); // 残余外部符号を新しい外部バッファ先端へコピー
... // 古い外部バッファを削除して_M_ext_bufと_M_ext_buf_sizeを新しいバッファに変更
}
else if (__remainder) __builtin_memmove(_M_ext_buf, _M_ext_next, __remainder); // 残余外部符号を外部バッファ先頭へコピー
_M_ext_next = _M_ext_buf; // _M_ext_nextを外部バッファ先頭へ移動
_M_ext_end = _M_ext_buf + __remainder; // _M_ext_endを残余外部符号終端へ移動
_M_state_last = _M_state_cur; // バッファ先頭のcodecvt状態を保存
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; // ファイル終端ならEOFフラグをセット
else if (__elen == -1) break; // エラーなら__ilen=0のままループを抜ける
_M_ext_end += __elen; // _M_ext_endを移動
}
char_type* __iend = this->eback(); // 内部バッファ符号領域終端
if (_M_ext_next < _M_ext_end) // _M_ext_next<_M_ext_endならcodecvt::inでコード変換
__r = _M_codecvt->in(_M_state_cur, // _M_state_curは更新
_M_ext_next, _M_ext_end, _M_ext_next, // _M_ext_nextは更新
this->eback(), this->eback() + __buflen, __iend); // __iendは更新
if (__r == codecvt_base::noconv) {...} // 無変換なら自力でコピーして__ilenを含む各メンバ変数を更新する
else __ilen = __iend - this->eback(); // __ilenの計算
if (__r == codecvt_base::error) break; // エラーでループを抜けるがエラー判定は__rよりも__ilen>0が優先
__rlen = 1; // 次ループで外部符号を1個追加、下のwhile文コメント参照の事
}
while (__ilen == 0 && !__got_eof); // __ilen>0かEOFフラグ成立まで繰り返す
}
if (__ilen > 0) // 変換に成功した場合
{
_M_set_buffer(__ilen); // setg(_M_buf,_M_buf,_M_buf+__ilen)
_M_reading = true; // _M_readingをセット
__ret = traits_type::to_int_type(*this->gptr()); // __retに*gptr()を代入
}
else if (__got_eof) // ファイル読み込み終了の場合
{
_M_set_buffer(-1); // setg(_M_buf,_M_buf,_M_buf)
_M_reading = false; // _M_readingをリセット
if (__r == codecvt_base::partial) {...} // __rがpartialなら例外送出
}
else if (__r == codecvt_base::error) {...} // __rが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 13.2.0 Manual 6.59 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/*追加する符号、デフォルトはEOF*/)
{
int_type __ret = traits_type::eof(); // 返値
const bool __testeof = traits_type::eq_int_type(__c, __ret); // __cがEOFである事を示すフラグ
const bool __testout = (_M_mode & ios_base::out || _M_mode & ios_base::app);
if (__testout) // ファイルがios_base::outかios_base::appで開かれている場合
{
if (_M_reading) {...} // 読み込み中からコールされた場合は読み込み処理をリセット
if (this->pbase() < this->pptr()) // pbase()<pptr()の場合
{
if (!__testeof) {...} // __cが非EOFなら内部符号列に追加、pptr()==epptr()ならオーバーフロー符号領域を使う
if (_M_convert_to_external(this->pbase(), this->pptr() - this->pbase())) // 変換してファイル書き込み
{
_M_set_buffer(0); // setp(_M_buf,_M_buf+_M_buf_size-1)、オーバーフロー符号領域を残す
__ret = traits_type::not_eof(__c);
}
}
else if (_M_buf_size > 1) // Unbuffered(説明省略)でなくpbase()==pptr()の場合
{
_M_set_buffer(0); // setp(_M_buf,_M_buf+_M_buf_size-1)、オーバーフロー符号領域を残す
_M_writing = true; // _M_writingをセット
if (!__testeof) {...} // __cが非EOFなら内部符号列に追加
__ret = traits_type::not_eof(__c);
}
else {...} // Unbuffered(説明省略)の場合の処理
}
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(); // 外部バッファサイズ=__ilen*最大符号長
char* __buf = static_cast<char*>(__builtin_alloca(__blen)); // 外部バッファ生成(スコープ自動解放メモリ)
char* __bend; // 外部バッファ符号列終端
const char_type* __iend; // 変換済み内部バッファ符号列終端
codecvt_base::result __r; // codecvt::outの返値
__r = _M_codecvt->out(_M_state_cur, // _M_state_curの更新
__ibuf, __ibuf + __ilen, __iend, // __iendの取得
__buf, __buf + __blen, __bend); // __bendの取得
if (__r == codecvt_base::ok || __r == codecvt_base::partial) // 変換された場合
__blen = __bend - __buf; // 外部バッファ符号列サイズの計算
else if (__r == codecvt_base::noconv) {...} // 無変換なら__bufに__ibuf代入、他メンバ変数も設定
else {...} // __rがerrorで例外送出
__elen = _M_file.xsputn(__buf, __blen); // ファイルへ外部バッファの符号列を書き込む
__plen = __blen; // 変換生成した外部符号数の取得
if (__r == codecvt_base::partial && __elen == __plen) // partialなら残余内部符号列をもう一度だけ変換
{...} // N4659 30.9.2.4/p10.3と思われるが外部バッファサイズが十分大きく実効しないのでは
}
return __elen == __plen; // 成功なら__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>>{}};
// ,new TMyCodeCvt<TMyCodeCvtStateICU<NMyEncoding::UTF16,NMyEncoding::UTF8>>{}};
auto fOut=std::wofstream{};
fOut.imbue(loc);
fOut.rdbuf()->pubsetbuf(buf.ptr_,bufSize); // Setbuf before open.
fOut.open(fileName);
fOut<<text<<std::flush; // Flush before checking fOut.
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 appropriate, append the overflow char.
if (!__testeof)
{
*this->pptr() = traits_type::to_char_type(__c);
this->pbump(1);
}
// Unconverted sequence which will be obtained by _M_convert_to_external.
const char_type* __iend;
// Convert pending sequence to external representation, and output.
if (_M_convert_to_external(this->pbase(), this->pptr() - this->pbase(), __iend))
{
// Unconverted sequence is moved to buffer beginning.
// Because _M_set_buffer() resets pptr(), unconverted length
// must be calculated before that.
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)
{
// Overflow in 'uncommitted' mode: set _M_writing, set the buffer
// to the initial 'write' mode, and put __c into the buffer.
_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
{
// Unbuffered.
__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;
// Sizes of external and pending output.
streamsize __elen;
streamsize __plen;
if (__check_facet(_M_codecvt).always_noconv())
{
__elen = _M_file.xsputn(reinterpret_cast<char*>(__ibuf), __ilen);
__plen = __ilen;
}
else
{
// Worst-case number of external bytes needed.
// XXX Not done encoding() == -1.
streamsize __blen = __ilen * _M_codecvt->max_length();
char* __buf = static_cast<char*>(__builtin_alloca(__blen));
char* __bend;
//const char_type* __iend; // Original definition of __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)
{
// Same as the always_noconv case above.
__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;
// Try once more for partial conversions.
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_H_INCLUDED

テストコード

ストリームバッファを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>>{}};
// ,new TMyCodeCvt<TMyCodeCvtStateICU<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; // Flush before checking fOut.
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/*ストリームに戻す符号、デフォルトはEOF*/)
{
int_type __ret = traits_type::eof(); // 返値
const bool __testin = _M_mode & ios_base::in;
if (__testin) // ファイルがios_base::inで開かれている場合
{
if (_M_writing) {...} // 書き込み中からコールされた場合は書き込み処理をリセット
const bool __testpb = _M_pback_init; // pbackバッファの有無フラグ
const bool __testeof = traits_type::eq_int_type(__i, __ret); // __iがEOFである事を示すフラグ
int_type __tmp; // 今のgptr()より一つ前の内部符号
if (this->eback() < this->gptr()) // eback()<gptr()の場合
{
this->gbump(-1); // gptr()を一つ戻す
__tmp = traits_type::to_int_type(*this->gptr()); // __tmpを取得
}
else if (this->seekoff(-1, ios_base::cur) != pos_type(off_type(-1))) // seekoffでファイル位置を1内部符号分だけ戻す
{
__tmp = this->underflow(); // seekoffに成功してその位置から内部バッファ符号列を再取得
if (traits_type::eq_int_type(__tmp, __ret)) return __ret; // underflowに失敗してEOFを返す
}
else return __ret; // seekoffに失敗してEOFを返す
if (!__testeof && traits_type::eq_int_type(__i, __tmp)) __ret = __i; // __iがEOFでなく__i==__tmpなら__iを返す
else if (__testeof) __ret = traits_type::not_eof(__i); // __iがEOFでもノーエラーでnot_eof(__i)を返す
else if (!__testpb) // __iがEOFでなく__i!=__tempでpbackバッファが無い場合(あればはエラーでEOFを返す)
{
_M_create_pback(); // pbackバッファを作成、gptr()はpbackバッファを指す
_M_reading = true; // _M_readingを設定
*this->gptr() = traits_type::to_char_type(__i); // pbackバッファに__iを書き込む
__ret = __i; // __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; // codecvt::encoding()の値
if (_M_codecvt) __width = _M_codecvt->encoding();
if (__width < 0) __width = 0; // -1(ステートフル)は0(可変長)として扱う
pos_type __ret = pos_type(off_type(-1));
const bool __testfail = __off != 0 && __width <= 0; // __off!=0で可変長なら失敗する
if (this->is_open() && !__testfail)
{
... // __offから外部符号列移動量を計算してファイル位置を移動する
}
return __ret;
}

seekoff規格要件について

外部文字コード可変長あるいはステートフルでseekoffが失敗するのは規格要件(30.9.2.4/p13)なので順守して修正しない(そもそもN:M変換をファイルストリームバッファで使うのは規格外だが、そこは棚に上げて)。まとめれば本サイト提案のcodecvtを利用する場合、内部バッファ先頭で符号をsputbackcあるいはsungetcで入力ストリームに戻そうとすると失敗する。