パソコンでプログラミングしよう ウィンドウズC++プログラミング環境の構築
1.6.3.6(15)
標準ライブラリのファイルストリーム

C++標準ライブラリのファイルストリームについて文字コード変換を中心に確認する。

ストリームとはデータ入出力を文字列データの流れ(ストリーム)として抽象化したオブジェクトである。

ファイルストリームとファイルストリームバッファ

C++標準ライブラリはストリームとストリームバッファを区分し、ストリームは書式化、ストリームバッファはバッファリングを担当する。アプリケーションはストリームとデータ交換し、ストリームが関連付けられたストリームバッファとデータ交換する。ファイルストリームはファイルストリームバッファを内部に所有し、ファイルストリームバッファが最終的に物理ファイルとデータ交換する。

ファイルストリームにあずかる文字および文字列は内部(アプリケーションで処理するデータ)と外部(物理ファイルに保存されるデータ)で文字コードが一般に異なり、ファイルストリームバッファがその変換を担当する。ファイルストリーム、ファイルストリームバッファ共にクラステンプレートとして供給される(JTC1/SC22/WG21 N4659 30.9.1)。

template<class charT, class traits=char_traits<charT>> class basic_filebuf;
template<class charT, class traits=char_traits<charT>> class basic_ifstream;
template<class charT, class traits=char_traits<charT>> class basic_ofstream;
template<class charT, class traits=char_traits<charT>> class basic_fstream;
using filebuf=basic_fielbuf<char>;
using ifstream=basic_ifstream<char>;
using ofstream=basic_ofstream<char>;
using fstream=basic_fstream<char>;
using wfilebuf=basic_fielbuf<wchar_t>;
using wifstream=basic_ifstream<wchar_t>;
using wofstream=basic_ofstream<wchar_t>;
using wfstream=basic_fstream<wchar_t>;

テンプレート仮引数charTが内部で符号(文字を表現するデータ)を格納するデータ型(char、wchar_tなど)、traitsがcharTの特性定義クラス(N4659 24.2)である。traitsは符号や符号列の比較関数や操作関数などを定め(24.2.1/p1)、そのデフォルトはchar_traitsクラステンプレート(24.2)のcharTによる特殊化である。特にtraitsのtypedefメンバであるstate_type(traits::state_type)(24.2.2/p4)がファイルストリームの文字コード変換に重要な役割を担い、char_traits<charT>でこれはmbstate_t(24.2.3.1/p4)である。

ロケールとファセット

ロケールは様々な言語への対応(ローカライゼーション)をカプセル化するもので、C++標準ライブラリはその目的でlocaleクラスを用意する(N4659 25.3.1)。localeは複数のファセットを持つ。ファセットはローカライゼーションの様々な側面を実装する複数のクラスあるいはクラステンプレートで、全てlocale::facetクラスを共通の基底とする。ファセットのほとんどは書式化を指定し、例えばnum_putファセットは数値を文字列に書式化する方法を指定する(25.4.2.2/p1)。従ってファセットのほとんどは書式化を担当するストリームが利用するもので、ストリームバッファとは無関係と言える。

文字コード変換ファセット(codecvtファセット)は異なる文字コード間の相互変換を行う(25.4.1.4/p1)。ファイルストリームバッファはcodecvtファセットを使って内部文字コード(アプリケーションが処理するデータ)と外部文字コード(物理ファイルに保存されるデータ)を相互変換する(30.9.2/p5)。codecvtファセットはストリームが直接利用しない例外的なファセットで、データ入出力においてファイルストリームバッファのみが利用する特殊な存在と言える。しかしながら、ストリームのロケール設定(imbue)は同時に関連付けられたストリームバッファのロケール設定(pubimbue)を行うため(30.5.5.3/p9)、codecvtファセットもストリームロケール設定による方法が多く採られる。そのためその特殊性が意識される事は少ない。

auto fout1=std::wofstream{"output1.txt"};
fout1.imbue(std::locale{std::locale{},new my_codecvt{}});
auto fout2=std::wofstream{"output2.txt"};
fout2.rdbuf()->pubimbue(std::locale{std::locale{},new my_codecvt{}});
覚え書き
ストリームバッファには他に文字列ストリームバッファ(basic_stringbuf)がある(30.8.2)。文字列ストリームバッファはcodecvtファセットを利用できないが、サイト作成者は長く混同し両者を組み合わせて文字コード変換関数が作れると誤解していた。

ctypeファセットはC言語標準ライブラリのctype.h(JTC1/SC22/WG14 N1570 7.4)とwctype.h(N1570 7.30)をカプセル化し(N4659 25.4.1.1/p1)、文字の分類やマッピングなどを行う。ctypeファセットとcodecvtファセットはctypeカテゴリに分類され(N4659 25.3.1.1/p2)、標準ライブラリはそれぞれについてctypeクラステンプレートとcodecvtクラステンプレートを定義する。

template<typename charT> class ctype;
template<typename internT,typename externT,typename stateT> class codecvt;

テンプレート仮引数charTは符号を格納するデータ型、internTはその内部型(ファイルストリームにおいてはアプリケーションが処理するデータ型)、externTは外部型(ファイルストリームにおいては物理ファイルに保存されるデータ型)である。stateTはとりあえず、標準がmbstate_tであることのみを意識しよう。このmbstate_tは既述したようにファイルストリームのデフォルトでtraits::state_typeにtypedefされている。

<locale>のcodecvt

localeはctypeカテゴリに関し以下のファセットをデフォルトで所有する(N4659 25.3.1.1.1/p3)。ファイルストリームバッファは対応するcodecvtファセットを用いるが、codecvtファセットは派生クラスで置換することができる。

template<> class ctype<char>;
template<> class ctype<wchar_t>;
template<> class codecvt<char,char,mbstate_t>;
template<> class codecvt<char16_t,char,mbstate_t>;
template<> class codecvt<char32_t,char,mbstate_t>;
template<> class codecvt<wchar_t,char,mbstate_t>;

以下に規格定義動作(N4659 25.4.1.4/p3)と本サイトの使用するコンパイラmingw-w64実装動作を示す。codecvt<wchar_t,char,mbstate_t>の詳細はソースコード(mingw-w64の標準codecvt)に示す。なおC++20はchar8_tの追加でcodecvt<char16_t,char,mbstate_t>は非推奨となりcodecvt<char16_t,char8_t,mbstate_t>が代替となる。

codecvtファセット 内部型 外部型 規格 mingw-w64実装 ファイルストリームバッファ
codecvt<char,char,mbstate_t> char char 無変換 無変換 filebuf
codecvt<char16_t,char,mbstate_t> char16_t char UTF-16⇔UTF-8 UTF-16⇔UTF-8 basic_filebuf<char16_t>
codecvt<char32_t,char,mbstate_t> char32_t char UTF-32⇔UTF-8 UTF-32⇔UTF-8 basic_filebuf<char32_t>
codecvt<wchar_t,char,mbstate_t> wchar_t char 実装依存 setlocaleに従う wfilebuf

以下のサンプルコードは実装動作を確認する。ソースコードはUTF-8であるか、またはシフトJISで入力文字コードをCP932(-finput-charset=CP932)とする。出力ファイルはUTF-8である。wchar_t文字列はそのままでASCII以外は変換に失敗し失われ、setlocale(LC_CTYPE,"")すればシフトJISで出力して文字化けする。

#include <iostream>
#include <fstream>
template<typename T> void Output(const T* str,std::ios_base::openmode mode=std::ios_base::out)
{std::basic_ofstream<T>{"file_stream_output.txt",mode}<<str;}
int main()
{
auto append=std::ios_base::app;
Output("char文字列の出力\n");
Output(u"char16_t文字列の出力\n",append);
Output(U"char32_t文字列の出力\n",append);
Output(L"wchar_t文字列の出力\n",append);
return 0;
}
覚え書き
サンプルコードは改行にマニピュレータendlを使用していないが、これはbasic_ofstream<char16_t>とbasic_ofstream<char32_t>が実行時エラーを招くためである。localeクラスがデフォルトで所有するctype特殊化ファセットがctype<char>とctype<wchar_t>だけなので、例えば以下のコードは実行時エラーを招く。
int main()
{
auto fout=std::basic_ofstream<char16_t>{"output.txt"};
fout<<u"こんにちは世界!"<<std::endl;
}
endlがctype<char16_t>::widen関数をコールしようとするが(N4659 30.7.5.4/p1)、use_facet<ctype<char16_t>>(fout.getloc())が例外bad_castを送出する(N4659 25.3.2/p3)。エラーを避けるにはctype<char16_t>ファセットをストリームにimbueするが、このファセットはクラステンプレートの明示的特殊化でなければならない。実行できるミニマムなコードを示す。
namespace std
{
template<> struct ctype<char16_t>:locale::facet
{
static locale::id id;
char16_t widen(char c) const {return c;}
};
locale::id ctype<char16_t>::id;
}
int main()
{
auto fout=std::basic_ofstream<char16_t>{"output.txt"};
fout.imbue({fout.getloc(),new std::ctype<char16_t>{}});
fout<<u"こんにちは世界!"<<std::endl;
}

<codecvt>のcodecvt

ヘッダ<codecvt>はC++17(N4695 D.15)およびC++20ドラフト(N4861 D.20.1)で非推奨となったcodecvtファセットを定義する(N4659 D.15.1)。これらは全てcodecvt<Elem,char,mbstate_t>クラスを継承する。

template <class Elem,unsigned long Maxcode=0x10ffff,codecvt_mode Mode=(codecvt_mode)0> class codecvt_utf8;
template <class Elem,unsigned long Maxcode=0x10ffff,codecvt_mode Mode=(codecvt_mode)0> class codecvt_utf16;
template <class Elem,unsigned long Maxcode=0x10ffff,codecvt_mode Mode=(codecvt_mode)0> class codecvt_utf8_utf16;

Elemをwchar_tとした場合を一覧し、mingw-w64デフォルト(ワイド実行文字コードがUTF-16)の実装動作を確認する。

codecvtファセット 内部型 外部型 規格 mingw-w64実装 ファイルストリームバッファ
codecvt_utf8<wchar_t> wchar_t char UCS-2/UCS-4⇔UTF-8 UTF-16⇔UTF-8 wfilebuf
codecvt_utf16<wchar_t> wchar_t char UCS-2/UCS-4⇔UTF-16BE UTF-16⇔UTF-16BE wfilebuf
codecvt_utf16<wchar_t,0x10ffff,little_endian> wchar_t char UCS-2/UCS-4⇔UTF-16LE UTF-16⇔UTF-16LE wfilebuf
codecvt_utf8_utf16<wchar_t> wchar_t char UTF-16⇔UTF-8 UTF-16⇔UTF-8 wfilebuf

UCS-2/UCS-4はElemのサイズに依存しウィンドウズwchar_tは16ビットでUCS-2となる。mingw-w64はこれをいずれもUTF-16で実装する。これら全てcodecvt<wchar_t,char,mbstate_t>を継承し、wfilebufのcodecvtに置き換えることができる。これらを利用する事で、ユニコード文字列にwchar_tを使用するレガシーコードでもファイルストリーム出力を容易に行えたが、今や非推奨機能で将来に不安が残る。

codecvt_utf16は外部型charのバイト列で出力するので、UTF-16エンディアン定義が必要になる。BOMを先頭に付加するにはModeにgenerate_headerとconsume_headerを加える(N4659 D.15.2/p1)。codecvt_utf16のみファイルストリームをバイナリモードで開く必要がある(D.15.2/p3)。

codecvtのカスタマイズ

標準で対応できない文字コード変換は自らcodecvtファセットを作成しカスタマイズするが、その方法は二つある。

  • codecvtクラステンプレートの特殊化
  • ライブラリ標準codecvtファセットの置き換え

通常localeの設定はファイルストリームバッファ(pubimbue)でなくファイルストリーム(imbue)を用いるため、以下においてファイルストリームへの設定として説明するが、実際はファイルストリームバッファへの設定であることを改めて強調する。ファイルストリームおよびファイルストリームバッファの符号格納データ型はcharTとする。なお詳細はソースコード(codecvtファセット)に譲る。

codecvtクラステンプレートの特殊化は以下を手順の概要とする。

  1. traits::state_typeがmbstate_tでないファイルストリームを作成する。
  2. codecvt<charT,char,traits::state_type>特殊化を定義する。
  3. codecvt<charT,char,traits::state_type>を所有するlocaleを作成する。
  4. 作成したlocaleをファイルストリームに設定(imbue)する。

ライブラリ標準codecvtファセットの置き換えは以下を手順の概要とする。

  1. 標準的な(traits::state_typeがmbstate_tである)ファイルストリームを作成する。
  2. codecvt<charT,char,mbstate_t>を継承するcodecvtファセットを定義する。
  3. codecvt<charT,char,mbstate_t>を新たなcodecvtファセットで置き換えたlocaleを作成する。
  4. 作成したlocaleをファイルストリームに設定(imbue)する。