ファイルストリームバッファに用いるcodecvtファセットを自作する場合の実装方法の詳解を試みる。
ソースコード(自作のcodecvtファセット)のTMyCodeCvt.hはcodecvtをiconvライブラリを利用して実装する。そこで二つのスキームを提案したが、スキーム自体は利用ライブラリに依存せず一般化できる。
- codecvtクラステンプレートの特殊化(ライブラリ利用スキーム1)
- ライブラリ標準codecvtファセットの置き換え(ライブラリ利用スキーム2)
本記事はその方法を詳解するが、サンプルは引き続きTMyCodeCvt.hをベースとして内容の多くが重複する。本記事は新たにICUライブラリを利用する方法とウィンドウズAPIを利用する方法を追加する。
実装クラステンプレート
実装クラステンプレートはライブラリ利用スキーム1とスキーム2で共用するクラステンプレートで、codecvtクラステンプレートのプロテクテッド仮想メンバ関数(do_in、do_outなど)をパブリックに実装する。TMyCodeCvt.hでこれはiconvライブラリを利用するTMyCodeCvtStateIconvクラステンプレートであるが、本記事は他にICUライブラリを利用するTMyCodeCvtStateICUクラステンプレートとウィンドウズAPIを利用するTMyCodeCvtStateMSWAPIクラステンプレートを追加する。追加の方法はTMyCodeCvt.hへの追記、クライアントコードへの追記、専用インクルードファイルの作成など任意だが、(TMyCodeCvt.h追記を除き)いずれもTMyCodeCvt.hのインクルードを前提とする。本サイトは記述の都合上、インクルードファイルTMyCodeCvtExt.hで導入する。
クラステンプレート | ライブラリ | 内部文字コード(T) | 外部文字コード(U) | 備考 |
TMyCodeCvtStateIconv | iconv | 任意 | 任意 | TMyCodeCvt.h |
TMyCodeCvtStateICU | ICU | UTF-16 | 任意 | TMyCodeCvtExt.h |
TMyCodeCvtStateMSWAPI | ウィンドウズAPI | UTF-16 | 任意 |
各クラステンプレートはテンプレート仮引数としてTとUを持ち、それぞれ内部文字コードと外部文字コードを指定するクラスで、その実引数となるクラスはインスタンス構築を想定しなくても良い。文字コード指定をテンプレート仮引数とするのはスキーム1の制限で詳しくは後述する。ライブラリに文字コードを指定する文字列はT::encoding()あるいはU::encoding()で与え、文字コードのデータ保持型はT::typeあるいはU::typeで与える。iconvは任意二つの文字コード変換関数を用意し、ICUとウィンドウズAPIはUTF-16と任意一つの文字コード変換関数を用意する。任意と言いながら、各ライブラリが対応可能な文字コードに限定されるのは当然の事である。ユニコード(手法)アプリケーションのファイル入出力なら内部wchar_t、外部charに固定して一般性を失わなず、ICUとウィンドウズAPIの制限も考慮して、以降はこれを暗黙に仮定する。以降において任意文字コードによるTMyCodeCvtStateXXXクラステンプレートの特殊化を単にTMyCodeCvtStateXXXクラスと参照し、構築されたインスタンスをTMyCodeCvtStateXXXインスタンスと参照する。
TMyCodeCvt.hは文字コード指定クラスをNMyEncoding名前空間で定義する。このクラスは各クラステンプレートで共用するため、少なくとも本記事の検討ではiconvとICU共通の文字コード名を返さなければならない。ウィンドウズAPIはコードページ(整数値)で文字コード指定するため、文字コード指定クラスからコードページ(整数値)へ変換するメタ関数を追加する。なお必ずしもNMyEncoding名前空間の定義を使用する必要は無く、例えばKTxtEditプロジェクトはTMyCodeCvt.hをインクルードしながら便宜上、独自の文字コード指定クラスを用いる。
NMyEncoding名前空間(TMyCodeCvt.h)
namespace NMyEncoding
{
#define ENCODING_TRAITS(NAME,ENCODING,CHAR_T) \
struct NAME {using type=CHAR_T;static const char* encoding() {return ENCODING;}};
ENCODING_TRAITS(UTF16,"UTF-16LE",wchar_t);
ENCODING_TRAITS(UTF8,"UTF-8",char);
ENCODING_TRAITS(ShiftJIS,"CP932",char);
ENCODING_TRAITS(JIS,"ISO-2022-JP",char);
}
クラステンプレートに共通な要件
各クラステンプレート(TMyCodeCvtStateXXXクラステンプレート)はライブラリ利用スキーム1とスキーム2で共用されるため、それぞれの要件を同時に満たす必要がある。以下にクラステンプレートが共通に考慮すべき要件を説明する。
ライブラリ利用スキーム1はTMyCodeCvtStateXXXクラスをcodecvtクラステンプレートの第三仮引数Stateに実引数として与える。ところで文字列、ストリーム、ストリームバッファはCharTとTraitsをテンプレート仮引数とするクラステンプレートで、CharTが文字型データ、Traitsが文字特性(character traits)クラスである(JTC1/SC22/WG21 N4659 24.2/p3)。Traitsの実引数がtraitsクラスであるとすればファイルストリームバッファはStateの実引数をtraits::state_typeとしたcodecvtで文字コード変換を行う(30.9.2/p5)。ファイルストリームバッファがスキーム1を利用するにはtraits::state_typeをTMyCodeCvtStateXXXクラスにtypedefするため、TMyCodeCvtStateXXXクラスはtraits::state_typeの要件(24.2.2/p4)を満たさなければならない。本記事実装のほとんどはコピー要件から意味論(セマンティクス)で逸脱し、JIS(ISO-2022-JP)のようなステートフルな文字コードで問題となる。
- コピー代入可能(CopyAssignable)
- コピー構築可能(CopyConstructible)
- デフォルト構築可能(DefaultConstructible)
ライブラリ利用スキーム2はcodecvt<wchar_t,char,mbstate_t>派生クラスがTMyCodeCvtStateXXXインスタンスを所有する。codecvt派生クラスのプロテクテッドなメンバ関数がTMyCodeCvtStateXXXクラスのパブリックなメンバ関数をコールするが、codecvt派生クラスメンバ関数がconstなのでTMyCodeCvtStateXXXメンバ関数もconstでなければならない。TMyCodeCvtStateXXXメンバ関数はTMyCodeCvtStateXXXクラスの内部状態を変化させる可能性があり(ステートフルな文字コードのシフト状態を記憶する、あるいは部分符号シーケンスの状態を記憶する、以降合わせて変換状態と参照する)、これはconstからの逸脱と見なされる。
実用はどちらかのスキームでの使用に限定されるだろうし、その場合に他スキームの要件は不要であるだけでなく積極的に省いた方が良い。スキーム1であれば各メンバ関数のconstを省けるし、そうすればconstからの逸脱はそもそも問題とならない。スキーム2であればコピー代入/構築は不要で、なぜならcodecvtの基底クラスfacetがそれらを抑止しているためで、コピー代入/構築へ対応するためのコードはそもそも無用だ(さらにそもそもの話だが、スキーム2ならわざわざTMyCodeCvtStateXXXインスタンスに処理を移譲するよりもcodecvt派生クラスに直接実装する方が自然ではないか)。
iconvライブラリの利用
iconvライブラリを利用するためインクルードファイル(iconv.h)とライブラリファイル(例えばlibiconv.a)を必要とする。
- iconvライブラリは任意二つの文字コード間を変換する関数を用意する(iconv関数)。
- コンストラクタは入力用と出力用で二つの変換記述子(iconv_t型でiconv関数はこれを第一仮引数に受ける)を開く(iconv_open関数)。変換記述子はデストラクタで閉じる(iconv_close関数)が、スキーム1要件のコピー代入/構築で複数インスタンスが同じ変換記述子を閉じてエラーとなる。iconv_tはvoid*にtypedefされていて、二つの変換記述子をデリータiconv_closeとしたstd::shared_ptrスマートポインタに保持してこれを解決する。スキーム2ならこの処理は無用である。このコピーはシャローコピーで複数インスタンスが変換状態を共有し、これが問題を生じる場合がある。コピーの問題については後にまとめる。
- do_inメンバ関数とdo_outメンバ関数は文字コード変換するが、その実装はdo_ioメンバ関数テンプレートで共通化する。do_ioは第一仮引数に二つの変換記述子のどちらかを受ける。iconv関数は文字型にcharのみを許しwchar_tはキャストを必要とするため、do_ioはその処理も行う。
- iconv関数は出力バッファが不足の場合にE2BIG、部分符号シーケンスとなる場合にEINVALを返し、do_ioはどちらの場合もpartialを返す。EINVALの場合にfrom_nextは部分符号シーケンスの先頭へ移動し、すなわちソースコード(自作のcodecvtファセット)に示した部分変換スキーム2である。
- do_unshiftメンバ関数はステートフルな文字コードのシフト状態を初期状態に戻すためのエスケープシーケンスを出力する。iconv関数は第二実引数にヌルを与えればその動作を行うので、do_outの第一実引数に0を与えてコールする。
- do_lengthメンバ関数はdo_inで読み込む文字数を返す。これは一般にdo_inで実装できる。
- do_always_noconv、do_max_length、do_encodingメンバ関数は常に定数を返しstaticとする。これらはソースコード(N:M変換の安全性)で説明する。
TMyCodeCvtStateIconvクラステンプレート(TMyCodeCvt.h)
template<typename T,typename U> class TMyCodeCvtStateIconv
{
public:
using result=std::codecvt_base::result;
using intern_type=typename T::type;
using extern_type=typename U::type;
private:
std::shared_ptr<std::remove_pointer<iconv_t>::type> in_;
std::shared_ptr<std::remove_pointer<iconv_t>::type> out_;
public:
TMyCodeCvtStateIconv()
:in_{iconv_open(T::encoding(),U::encoding()),iconv_close}
,out_{iconv_open(U::encoding(),T::encoding()),iconv_close}
{}
private:
template<typename from_type,typename to_type> result do_io
(const iconv_t& cd
,const from_type* from,const from_type* from_end,const from_type*& from_next
,to_type* to,to_type* to_end,to_type*& to_next) const
{
if (from>from_end||to>to_end) {return std::codecvt_base::error;}
auto* fromBuf=reinterpret_cast<char*>(const_cast<from_type*>(from));
auto fromBytes=(from_end-from)*sizeof(from_type);
auto* toBuf=reinterpret_cast<char*>(to);
auto toBytes=(to_end-to)*sizeof(to_type);
auto iconvRet=iconv(cd,&fromBuf,&fromBytes,&toBuf,&toBytes);
from_next=reinterpret_cast<from_type*>(fromBuf);
to_next=reinterpret_cast<to_type*>(toBuf);
if (iconvRet==(size_t)(-1))
{
return (errno==E2BIG||errno==EINVAL)?
std::codecvt_base::partial:std::codecvt_base::error;
}
return std::codecvt_base::ok;
}
public:
result do_in
(const extern_type* from,const extern_type* from_end,const extern_type*& from_next
,intern_type* to,intern_type* to_end,intern_type*& to_next) const
{return do_io(in_.get(),from,from_end,from_next,to,to_end,to_next);}
result do_out
(const intern_type* from,const intern_type* from_end,const intern_type*& from_next
,extern_type* to,extern_type* to_end,extern_type*& to_next) const
{return do_io(out_.get(),from,from_end,from_next,to,to_end,to_next);}
result do_unshift(extern_type* to,extern_type* to_end,extern_type*& to_next) const
{
const auto* from_next=static_cast<intern_type*>(0);
return do_out(0,0,from_next,to,to_end,to_next);
}
int do_length(const extern_type* from,const extern_type* from_end,size_t max) const
{
const auto* from_next=from;
intern_type to[max];
auto* to_next=to;
do_in(from,from_end,from_next,to,to+max,to_next);
return from_next-from;
}
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;}
};
- 覚え書き
- do_lengthメンバ関数はto配列初期化をmax変数でサイズ指定するがC++規格外である。C言語規格はこれを許し(JTC1/SC22/WG14 N1570 6.7.6.2/p4)、C++が上位互換しない数少ない規格の一つであるが、mingw-w64はデフォルトでC++拡張に許す(GCC 14.1.0 Manual 6.20 Arrays of Variable Length)。サイト作成者は無意識にこれを利用するがご容赦いただきたい。
ICUライブラリの利用
ICUライブラリを利用するためインクルードファイル(unicode/ucnv.h)とライブラリファイル(例えばlibicuuc.aとlibicudt.a)を必要とする。
- ICUライブラリはUTF-16と任意文字コード間を相互変換する関数を用意する(ucnv_fromUnicode、ucnv_toUnicode関数)。プライマリテンプレートは定義せず、第一実引数をUTF-16に固定した部分特殊化で定義する。
- コンストラクタは変換オブジェクト(UConverter型へのポインタで文字コード変換関数はこれを第一仮引数に受ける、C++オブジェクトとは無関係)を開き(ucnv_open関数)、変換失敗した場合に代替文字を使用せずエラーとさせる(ucnv_setFromUCallBack、ucnv_setToUCallBack関数)。ICUはiconvと異なりクローン関数(ucnv_safeCloneまたはucnv_clone関数)を用意して、コピー構築/代入はこれを用いてディープコピーする。変換オブジェクトはデストラクタで閉じる(ucnv_close関数)が、デリーターをucnv_closeとしたstd::unique_ptrに保持し、複数インスタンスは変換状態を共有しない。
- do_inメンバ関数とdo_outメンバ関数の実装はdo_ioメンバ関数テンプレートで共通化する。do_ioは第一仮引数に文字コード変換関数のどちらかを受ける。ICUはワイド文字(ユニコード文字)に独自のUChar型を用いるためwchar_tと相互キャストする処理を必要とする。
- 文字コード変換関数の返すエラーコードはmutableなメンバ変数ec_に記憶する。エラーコードU_BUFFER_OVERFLOW_ERRORは出力バッファが不足の場合でdo_ioはpartialを返すが、継続する変換を行うためec_はリセットする。部分符号シーケンスとなる場合でもfrom_nextはfrom_endまで移動して部分符号シーケンスは変換オブジェクトが記憶する。すなわちソースコード(自作のcodecvtファセット)に示した部分変換スキーム1である。
- do_unshiftメンバ関数はTMyCodeCvtStateIconv同様にdo_outの第一実引数に0を与えてコールする。do_ioはその値を第二仮引数fromに受け、文字コード変換関数の第七実引数に!fromで渡す。文字コード変換関数は第七仮引数trueでフラッシュしてステートフルな文字コードであればシフトを初期状態に戻す。
- do_length、do_always_noconv、do_max_length、do_encodingメンバ関数はTMyCodeCvtStateIconvに倣う。
TMyCodeCvtStateICUクラステンプレート(TMyCodeCvtExt.h)
template<typename,typename> class TMyCodeCvtStateICU;
template<typename U> class TMyCodeCvtStateICU<NMyEncoding::UTF16,U>
{
static_assert(std::is_same<typename U::type,char>::value
,"External character type must be char.");
public:
using result=std::codecvt_base::result;
using intern_type=wchar_t;
using extern_type=char;
private:
mutable UErrorCode ec_;
std::unique_ptr<UConverter,void(*)(UConverter*)> cnv_;
public:
TMyCodeCvtStateICU()
:ec_{U_ZERO_ERROR},cnv_{ucnv_open(U::encoding(),&ec_),ucnv_close}
{
ucnv_setFromUCallBack(cnv_.get(),UCNV_FROM_U_CALLBACK_STOP
,nullptr,nullptr,nullptr,&ec_);
ucnv_setToUCallBack(cnv_.get(),UCNV_TO_U_CALLBACK_STOP
,nullptr,nullptr,nullptr,&ec_);
}
TMyCodeCvtStateICU(const TMyCodeCvtStateICU& rhs)
:ec_{rhs.ec_},cnv_{ucnv_safeClone(rhs.cnv_.get(),NULL,NULL,&ec_),ucnv_close}
{}
TMyCodeCvtStateICU& operator=(const TMyCodeCvtStateICU& rhs)
{
auto temp=TMyCodeCvtStateICU{rhs};
using std::swap;
swap(ec_,temp.ec_);
swap(cnv_,temp.cnv_);
return *this;
}
~TMyCodeCvtStateICU() {}
private:
template<typename F,typename from_type,typename to_type> result do_io
(F func
,const from_type* from,const from_type* from_end,const from_type*& from_next
,to_type* to,to_type* to_end,to_type*& to_next) const
{
if (from>from_end||to>to_end) {return std::codecvt_base::error;}
using TT1=typename
std::conditional<std::is_same<from_type,wchar_t>::value,UChar,from_type>::type;
using TT2=typename
std::conditional<std::is_same<to_type,wchar_t>::value,UChar,to_type>::type;
from_next=from;
to_next=to;
func(cnv_.get()
,reinterpret_cast<TT2**>(&to_next)
,reinterpret_cast<TT2*>(to_end)
,reinterpret_cast<const TT1**>(&from_next)
,reinterpret_cast<const TT1*>(from_end)
,nullptr,!from,&ec_);
if (U_SUCCESS(ec_)) {return std::codecvt_base::ok;}
if (ec_==U_BUFFER_OVERFLOW_ERROR)
{ec_=U_ZERO_ERROR;return std::codecvt_base::partial;}
return std::codecvt_base::error;
}
public:
result do_in
(const extern_type* from,const extern_type* from_end,const extern_type*& from_next
,intern_type* to,intern_type* to_end,intern_type*& to_next) const
{return do_io(ucnv_toUnicode,from,from_end,from_next,to,to_end,to_next);}
result do_out
(const intern_type* from,const intern_type* from_end,const intern_type*& from_next
,extern_type* to,extern_type* to_end,extern_type*& to_next) const
{return do_io(ucnv_fromUnicode,from,from_end,from_next,to,to_end,to_next);}
result do_unshift(extern_type* to,extern_type* to_end,extern_type*& to_next) const
{
const auto* from_next=static_cast<intern_type*>(0);
return do_out(0,0,from_next,to,to_end,to_next);
}
int do_length(const extern_type* from,const extern_type* from_end,size_t max) const
{
const auto* from_next=from;
intern_type to[max]={};
auto* to_next=to;
do_in(from,from_end,from_next,to,to+max,to_next);
return from_next-from;
}
static bool do_always_noconv() {return false;}
static int do_max_length() {return MB_LEN_MAX;}
static int do_encoding() {return 0;}
};
ウィンドウズAPIの利用
ウィンドウズAPIの利用でインクルードファイル(stringapiset.h)とライブラリファイル(Kernel32.lib)を必要とするが、これらはウィンドウズデスクトップアプリケーション開発に当然のように含まれているはずである。
- ウィンドウズAPIはUTF-16と任意文字コード間を相互変換する関数を用意する(WideCharToMultiByte、MultiByteToWideChar関数)。プライマリテンプレートは定義せず、第一実引数をUTF-16に固定した部分特殊化で定義する。
- 文字コード変換関数への文字コード指定は名前ではなくコードページ(整数値)で指定する。変換成功の判断には最大符号長も必要とする。NMyEncoding名前空間に定義するTraitsExはそのためのメタ関数で、NMyEncoding名前空間に定義した文字コード指定クラスから各情報を知る事ができる。
- 文字コード変換関数は出力バッファが不足でもバッファフルまで変換できるが、入力をどこまで読み取ったか知る手段が無い。このためストリームバッファに利用するには1文字ずつ処理して入力データ数をカウントしていく必要がある。文字コード変換関数はステートフルな文字コードを変換できるがシフト状態を記憶する手段が無く、複数回コールでの変換は難しい。一般にストリームバッファは入力を任意に分割して複数回の処理を行うが、本ケースはさらに悪い事に1文字ずつの処理を行い、ステートフルな文字コードを扱う事はほとんど不可能である。
- do_inメンバ関数とdo_outメンバ関数の実装はdo_ioメンバ関数テンプレートで共通化する。do_ioは第一仮引数に文字コード変換関数のどちらかを受けるが、二つの文字コード変換関数の仮引数リストが対称でない(WideCharToMultiByteの仮引数の数はMultiByteToWideCharより2個多い)ため、MyMultiByteToWideCharとMyWideCharToMultiByteというスタティックメンバ関数で仮引数リストを揃える。do_ioの第二仮引数は変換元文字コードの最大符号長を受ける。do_ioは変換元データを一符号ずつ読み込み最大符号長に達するまでに変換成功すれば1文字変換に成功したとして変換先データに加え、入力バッファを読み切るか出力バッファフルになるまで繰り返す。最大符号長に達して変換成功しない場合はerrorを返し、文字コード変換関数がERROR_INSUFFICIENT_BUFFERで失敗する場合はpartialを返し、変換成功して入力バッファを読み切った場合はokを返し、変換成功して出力バッファフルになればpartialを返す。ERROR_INSUFFICIENT_BUFFERでfrom_nextは部分符号シーケンスの先頭に留まり、すなわちソースコード(自作のcodecvtファセット)に示した部分変換スキーム2である。
- MyMultiByteToWideCharはMultiByteToWideCharの第二実引数にMB_ERR_INVALID_CHARSを与えて変換できない文字はエラーとする。
- MyWideCharToMultiByteは文字コードに依存して処理を切り替える必要がある。UTF-8(およびGB18030)以外はWideCharToMultiByteの第二実引数に0を与えて第八実引数にアドレスを与えたusedDefaultCharがTRUEで返れば変換できない文字が代替文字に置き換わったとしてエラーとする。UTF-8(およびGB18030)はMyWideCharToMultiByteの明示的特殊化で第二実引数にWC_ERR_INVALID_CHARSを与えて変換できない文字はエラーとする。
- do_unshiftメンバ関数はTMyCodeCvtStateIconv同様にdo_outの第一実引数に0を与えてコールする。do_ioはその値を第三仮引数fromに受けるが、ステートフル文字コードをそもそも扱えないので何もせずに返す。
- do_length、do_always_noconv、do_max_length、do_encodingメンバ関数はTMyCodeCvtStateIconvに倣う。
- 覚え書き
- ウィンドウズは他にMLangサービスに文字コード変換ルーチンを用意する。その文字コード変換関数は読み取った入力文字数を返し、ステートフルな文字コードのシフト状態を記憶できるので、codecvtへの実装により適しているかに見える。ところが出力バッファが不足の場合にはバッファフルまで変換するにもかかわらず、エラーを報告した上に読み取った入力文字数として0を返す。つまりWideCharToMultiByte/MultiByteToWideChar関数に対する優位性はほどんど無い。マイクロソフトはMLangを既に非推奨として、mingw-w64実装も不完全なので(32ビット版のライブラリファイルが欠落している、64ビット版はインターフェースIDが未定義参照になる)、本記事はMLangを無視する。
NMyEncoding::TraitsExメタ関数(TMyCodeCvtExt.h)
namespace NMyEncoding
{
#define ENCODING_TRAITSEX(NAME,CODEPAGE,LENMAX) \
template<> constexpr DWORD TraitsEx<NAME>::codepage() {return CODEPAGE;} \
template<> constexpr int TraitsEx<NAME>::lenmax() {return LENMAX;}
template<typename> struct TraitsEx
{
static constexpr DWORD codepage();
static constexpr int lenmax();
};
ENCODING_TRAITSEX(UTF16,1200,2);
ENCODING_TRAITSEX(UTF8,65001,4);
ENCODING_TRAITSEX(ShiftJIS,932,2);
ENCODING_TRAITSEX(JIS,50222,5);
}
TMyCodeCvtStateMSWAPIクラステンプレート(TMyCodeCvtExt.h)
template<typename,typename> class TMyCodeCvtStateMSWAPI;
template<typename U> class TMyCodeCvtStateMSWAPI<NMyEncoding::UTF16,U>
{
static_assert(std::is_same<typename U::type,char>::value
,"External character type must be char.");
public:
using result=std::codecvt_base::result;
using intern_type=wchar_t;
using extern_type=char;
private:
static constexpr int MultiByteCodePage() {return NMyEncoding::TraitsEx<U>::codepage();}
static int MyMultiByteToWideChar
(const extern_type* lpMultiByteStr,int cbMultiByte
,intern_type* lpWideCharStr,int cchWideChar,bool& insufficientBuffer)
{
auto retVal=::MultiByteToWideChar(MultiByteCodePage()
,MB_ERR_INVALID_CHARS,lpMultiByteStr,cbMultiByte,lpWideCharStr,cchWideChar);
insufficientBuffer=(::GetLastError()==ERROR_INSUFFICIENT_BUFFER);
return retVal;
}
static int MyWideCharToMultiByte
(const intern_type* lpWideCharStr,int cchWideChar
,extern_type* lpMultiByteStr,int cbMultiByte,bool& insufficientBuffer)
{
auto usedDefaultChar=BOOL{false};
auto retVal=::WideCharToMultiByte(MultiByteCodePage()
,0,lpWideCharStr,cchWideChar,lpMultiByteStr,cbMultiByte,0,&usedDefaultChar);
insufficientBuffer=(::GetLastError()==ERROR_INSUFFICIENT_BUFFER);
return usedDefaultChar?0:retVal;
}
template<typename F,typename from_type,typename to_type> result do_io
(F func,int chLenmax
,const from_type* from,const from_type* from_end,const from_type*& from_next
,to_type* to,to_type* to_end,to_type*& to_next) const
{
if (from>from_end||to>to_end) {return std::codecvt_base::error;}
if (!from) {return std::codecvt_base::ok;}
from_next=from;to_next=to;
if (from_next==from_end) {return std::codecvt_base::ok;}
for (auto continueOuter=true;continueOuter;)
{
continueOuter=false;
for (auto from_n=1;from_n<=from_end-from_next;++from_n)
{
auto insufficientBuffer=false;
if (auto to_n=func(from_next,from_n,to_next,to_end-to_next,insufficientBuffer))
{
from_next+=from_n;to_next+=to_n;
if (from_next==from_end) {return std::codecvt_base::ok;}
if (to_next==to_end) {return std::codecvt_base::partial;}
continueOuter=true;
break;
}
if (insufficientBuffer) {return std::codecvt_base::partial;}
if (from_n==chLenmax) {return std::codecvt_base::error;}
}
}
return std::codecvt_base::partial;
}
public:
result do_in
(const extern_type* from,const extern_type* from_end,const extern_type*& from_next
,intern_type* to,intern_type* to_end,intern_type*& to_next) const
{
return do_io(MyMultiByteToWideChar,NMyEncoding::TraitsEx<U>::lenmax()
,from,from_end,from_next,to,to_end,to_next);
}
result do_out
(const intern_type* from,const intern_type* from_end,const intern_type*& from_next
,extern_type* to,extern_type* to_end,extern_type*& to_next) const
{
return do_io(MyWideCharToMultiByte,NMyEncoding::TraitsEx<NMyEncoding::UTF16>::lenmax()
,from,from_end,from_next,to,to_end,to_next);
}
result do_unshift(extern_type* to,extern_type* to_end,extern_type*& to_next) const
{
const auto* from_next=static_cast<intern_type*>(0);
return do_out(0,0,from_next,to,to_end,to_next);
}
int do_length(const extern_type* from,const extern_type* from_end,size_t max) const
{
const auto* from_next=from;
intern_type to[max]={};
auto* to_next=to;
do_in(from,from_end,from_next,to,to+max,to_next);
return from_next-from;
}
static bool do_always_noconv() {return false;}
static int do_max_length() {return MB_LEN_MAX;}
static int do_encoding() {return 0;}
};
template<> inline int TMyCodeCvtStateMSWAPI
<NMyEncoding::UTF16,NMyEncoding::UTF8>::MyWideCharToMultiByte
(const intern_type* lpWideCharStr,int cchWideChar
,extern_type* lpMultiByteStr,int cbMultiByte,bool& insufficientBuffer)
{
auto retVal=::WideCharToMultiByte(MultiByteCodePage()
,WC_ERR_INVALID_CHARS,lpWideCharStr,cchWideChar,lpMultiByteStr,cbMultiByte,0,0);
insufficientBuffer=(::GetLastError()==ERROR_INSUFFICIENT_BUFFER);
return retVal;
}
traits::state_typeコピーの問題
traits::state_typeはコピー構築/代入可能を要件とする。ライブラリ利用スキーム1でtraits::state_typeはTMyCodeCvtStateXXXクラスであり、スキーム2でmbstate_tである。本記事に提案する実装のほとんどは統語論(シンタックス)で要件を満たすのみで意味論(セマンティクス)で逸脱する。traits::state_typeは変換状態の記憶を目的とするが、そもそもスキーム2はTMyCodeCvtStateXXXインスタンスをcodecvtクラスのメンバ変数に保持してmbstate_tのコピーに何の意味もない。スキーム1もTMyCodeCvtStateIconvクラスのようなシャローコピーは複数のインスタンスが同じ変換状態を指し、コピー要件から意味論で逸脱する。
以下にコピーが重要となる場所を説明するが、ファイルストリームバッファ(basic_filebufクラステンプレート)のメンバ関数、メンバ変数についてはソースコード(N:M変換の安全性)で説明している。traits::state_typeのコピーはbasic_filebufが外部符号列を読み込む(underflowメンバ関数)際に行われる。underflowはファイルから外部バッファに読み込み、文字コード変換して内部バッファに書き込むが、その際に開始時と終了時の変換状態としてtraits::state_typeを二つのメンバ変数(_M_state_last、_M_state_curメンバ変数)に記憶する。ここで_M_state_lastは前回underflowコール時の_M_state_curのコピーである。内部バッファ位置(gptr())から外部バッファ位置を知るため、_M_state_lastをcodecvt::lengthメンバ関数の第一実引数に与える。スキーム2は変換状態をcodecvtクラスのメンバ変数に記憶するため、スキーム1でシャローコピーの場合は_M_state_lastが_M_state_curと同じ変換状態を指すため、以下の場合に誤った変換状態をcodecvt::lengthに与え正しく計算できない。
- ステートフルな文字コード
- ソースコード(自作のcodecvtファセット)の部分変換スキーム1
ライブラリ利用スキーム1の場合、TMyCodeCvtStateICUはディープコピーで問題は生じない。TMyCodeCvtStateIconvはシャローコピーだが部分変換スキーム2(部分符号シーケンスを変換状態に記憶しない)なので、ステートフルな文字コードの場合のみ問題となる。ライブラリ利用スキーム2の場合、TMyCodeCvtStateIconvとTMyCodeCvtStateICU共にステートフルな文字コードが問題となる。TMyCodeCvtStateICUは部分変換スキーム1(部分符号シーケンスを変換状態に記憶する)なのでステートフルでない文字コードでも問題となる。なおTMyCodeCvtStateMSWAPIはそもそも変換状態を記憶せずステートフルな文字コードが扱えない。
gptr()から外部バッファ位置を求める処理は二箇所で確認できる。第一は外部符号列をファイルに書き込む(overflowメンバ関数)際、読み込み中から書き込みに移る場合にファイル位置を外部バッファに残された外部符号(gptr()に対応)の数だけ戻す処理で行う。第二はロケールを設定する(imbueメンバ関数)際、既に読み込み中にある場合に残された外部符号列を外部バッファ先頭に移動する処理で行う。前者は同一ファイルストリームの入出力方向切り替えであり、後者はファイルストリーム入力途中でのファセット(例えば文字コード変換ファセット)変更であり、それほど一般的な操作ではない。
ここでの結論をまとめる。"扱えるが制限あり"は双方向ファイルストリームが使えず、ストリーム途中でのファセット変更ができない。なおこの表はソースコード(N:M変換の安全性)に示した他の制約を反映していない。
スキーム | ライブラリ | 実装クラステンプレート | ステートフルな文字コード | ステートフルでない文字コード |
スキーム1 | iconv | TMyCodeCvtStateIconv | 扱えるが制限あり | 扱える |
ICU | TMyCodeCvtStateICU | 扱える | 扱える |
ウィンドウズAPI | TMyCodeCvtStateMSWAPI | 扱えない | 扱える |
スキーム2 | iconv | TMyCodeCvtStateIconv | 扱えるが制限あり | 扱える |
ICU | TMyCodeCvtStateICU | 扱えるが制限あり | 扱えるが制限あり |
ウィンドウズAPI | TMyCodeCvtStateMSWAPI | 扱えない | 扱える |
ベンチマーク
ここまで来たらベンチマークを取って比較しなければならない。検証パソコンにおいて64ビットリリースビルドしたテスト用アプリケーションで処理時間を比較した。サンプルはおよそ800万文字からなるUTF-16文字列で、これをUTF-8/シフトJIS/JISへ変換する処理時間とUTF-16へ戻す処理時間を計測した。参考比較としてUTF-8にはC++標準ライブラリのcodecvt_utf8_utf16<wchar_t>ファセット、シフトJISにはcodecvt<wchar_t,char,mbstate_t>ファセットを追加した。データは全てメモリ内に保持し十分大きなバッファサイズで一括変換させた。計測は5回行い平均値を結果とした。
* | ライブラリ | 実装クラステンプレート | UTF-16 → * | * → UTF-16 |
UTF-8 | iconv | TMyCodeCvtStateIconv | 97 ms | 82 ms |
ICU | TMyCodeCvtStateICU | 31 ms | 87 ms |
ウィンドウズAPI | TMyCodeCvtStateMSWAPI | 253 ms | 814 ms |
(参考)codecvt_utf8_utf16<wchar_t> | 57 ms | 54 ms |
シフトJIS | iconv | TMyCodeCvtStateIconv | 189 ms | 144 ms |
ICU | TMyCodeCvtStateICU | 39 ms | 51 ms |
ウィンドウズAPI | TMyCodeCvtStateMSWAPI | 294 ms | 465 ms |
(参考)codecvt<wchar_t,char,mbstate_t> | 594 ms | 713 ms |
JIS | iconv | TMyCodeCvtStateIconv | 155 ms | 126 ms |
ICU | TMyCodeCvtStateICU | 192 ms | 131 ms |
ウィンドウズAPI | TMyCodeCvtStateMSWAPI | エラー | エラー |
ウィンドウズAPIは文字単位処理の繰り返しなので他と比較すれば遅く、ステートフルな文字コードであるJISを処理できない。iconvとICUの優劣はつけ難い。シフトJISで参考としたcodecvt<wchar_t,char,mbstate_t>ファセット(標準ワイド文字列codecvtファセット)は非常に遅いが、mingw-w64実装の問題であると考えられる。
codecvtクラステンプレートの特殊化(ライブラリ利用スキーム1)
ライブラリ利用スキーム1はcodecvtテンプレートの特殊化で、第三テンプレート仮引数(State)にデフォルトmbstate_tの代わりに実装クラステンプレート(TMyCodeCvtStateXXXクラステンプレート)のいずれかのインスタンス(TMyCodeCvtStateXXXクラス)を実引数として与える(codecvt<wchar_t,char,TMyCodeCvtStateXXX<T,U>>)。std名前空間で定義し、標準ファセット(N4659 25.3.1.1.1/p2)でないためidスタティックメンバ変数(25.3.1.1.2/p1)定義も追加する。TMyCodeCvtStateXXXインスタンスはクライアント側でデフォルト構築され、各メンバ関数の第一仮引数に参照渡しされる。デフォルト構築なのでコンストラクタへ引数を渡せず、このため文字コード指定をテンプレート引数で行わざるを得ない。第一仮引数に渡されるTMyCodeCvtStateXXXインスタンスは非const参照なので各パブリックメンバ関数はconstである必要は無く、その逸脱は意味論においても問題とならない。なおmingw-w64標準ライブラリ拡張の__codecvt_abstract_baseクラステンプレート(GCC 14.1.0 Standard C++ Library Reference Manual 5.9 std::__codecvt_abstract_base<_InternT,_ExternT,_StateT> Class Template Reference)を継承してパブリックメンバ関数の定義を省略している。
codecvt<wchar_t,char,TMyCodeCvtStateXXX<T,U>>クラステンプレート特殊化(TMyCodeCvt.h)
namespace std
{
template <template<typename,typename> class CodeCvtStateTemplate,typename T,typename U>
class codecvt
<typename CodeCvtStateTemplate<T,U>::intern_type
,typename CodeCvtStateTemplate<T,U>::extern_type
,CodeCvtStateTemplate<T,U>>
:public __codecvt_abstract_base
<typename CodeCvtStateTemplate<T,U>::intern_type
,typename CodeCvtStateTemplate<T,U>::extern_type
,CodeCvtStateTemplate<T,U>>
{
public:
using result=codecvt_base::result;
using intern_type=typename CodeCvtStateTemplate<T,U>::intern_type;
using extern_type=typename CodeCvtStateTemplate<T,U>::extern_type;
using state_type=CodeCvtStateTemplate<T,U>;
public:
static locale::id id;
public:
explicit codecvt(size_t refs=0)
:__codecvt_abstract_base<intern_type,extern_type,state_type>{refs}
{}
protected:
~codecvt() override {}
result do_in
(state_type& state,const extern_type* from,const extern_type* from_end,const extern_type*& from_next
,intern_type* to,intern_type* to_end,intern_type*& to_next) const override
{return state.do_in(from,from_end,from_next,to,to_end,to_next);}
result do_out
(state_type& state,const intern_type* from,const intern_type* from_end,const intern_type*& from_next
,extern_type* to,extern_type* to_end,extern_type*& to_next) const override
{return state.do_out(from,from_end,from_next,to,to_end,to_next);}
result do_unshift
(state_type& state,extern_type* to,extern_type* to_end,extern_type*& to_next) const override
{return state.do_unshift(to,to_end,to_next);}
int do_length
(state_type& state,const extern_type* from,const extern_type* from_end,size_t max) const override
{return state.do_length(from,from_end,max);}
bool do_always_noconv() const noexcept override {return state_type::do_always_noconv();}
int do_max_length() const noexcept override {return state_type::do_max_length();}
int do_encoding() const noexcept override {return state_type::do_encoding();}
};
template<template<typename,typename> class CodeCvtStateTemplate,typename T,typename U>
locale::id codecvt
<typename CodeCvtStateTemplate<T,U>::intern_type
,typename CodeCvtStateTemplate<T,U>::extern_type
,CodeCvtStateTemplate<T,U>>::id;
}
メリット
メリットはcodecvtファセット要件と無関係で、constからの逸脱が問題とならない。ファセットは内部に状態を持たず、複数のファイルストリーム(正確にはファイルストリームバッファで主にはwfilebuf)はこれを安全に共有する。
デメリット
第一のデメリットは既に述べていて、TMyCodeCvtStateXXXインスタンスをデフォルト構築するのでコンストラクタへ引数を渡せない。使用する文字コードはコンパイル時に決定され、実行時の動的設定は不可能である。なおmingw-w64は全く同じ目的で__gnu_cxx::encoding_stateクラスとこれを第三テンプレート実引数としたcodecvtの部分特殊化を拡張として供給する(GCC 14.1.0 Standard C++ Library Manual 8.2.2 codecvt、GCC 14.1.0 Standard C++ Library Reference Manual 5.419 __gnu_cxx::encoding_state Class Reference)。ただしTMyCodeCvtStateXXXクラステンプレートと異なり文字コードをコンストラクタ仮引数に受ける。サンプルコード(GCC 14.1.0 Standard C++ Library Manual 8.2.2.4 Use)は自らコンストラクタ実引数を与えて構築したencoding_stateインスタンスをcodecvt::in関数の第一実引数に与えるメモリ内変換の例示に留まり、ファイルストリーム(あるいはwfilebuf)での利用は示されない。ファイルストリームでの課題としてencoding_stateクラスを規格に沿って初期化する方法が分からない(GCC 14.1.0 Standard C++ Library Manual 8.2.2.5 Future、原文はhow to initialize the state object in a standards-conformant manner?)として、明らかに同じ問題に突き当たっている。
第二のデメリットとしてこれが利用するクラステンプレート特殊化の多くで、例えば以下のようなクラスの特殊化も必要とする。
using TMyState=TMyCodeCvtStateIconv<NMyEncoding::UTF16,NMyEncoding::UTF8>;
struct TMyTraits:std::char_traits<wchar_t>
{
using pos_type=std::fpos<TMyState>;
using state_type=TMyState;
};
例えばwfstreamはbasic_fstream<wchar_t>のtypedefだが、実はテンプレート第二仮引数(Traits)にデフォルト実引数を与えたbasic_fstream<wchar_t,char_traits<wchar_t>>である。議論のcodecvt特殊化がTraitsテンプレート仮引数を直接あるいは間接に持つクラステンプレートを利用するには、デフォルトでなくTMyTraitsで特殊化したインスタンスでなければならない。これは暗黙的特殊化で十分なので単に実体化すれば良い。特殊化の必要なクラステンプレートは多数にのぼり代表例を示すが、それぞれTraits(デフォルトはchar_traits<wchar_t>)をTMyTraitsで特殊化する。
種類 | クラステンプレート | wchar_tのデフォルト |
ストリーム | template<class CharT,class Traits=char_traits<CharT>> class basic_fstream | wfstream=basic_fstream<wchar_t,char_traits<wchar_t>> |
バッファ | template<class CharT,class Traits=char_traits<CharT>> class basic_filebuf | wfilebuf=basic_fielbuf<wchar_t,char_traits<wchar_t>> |
文字列 | template<class CharT,class Traits=char_traits<CharT>, class Allocator=allocator<CharT>> class basic_string | wstring=basic_string<wchar_t,char_traits<wchar_t>> |
ファセット | template<class CharT,class InputIterator=istreambuf_iterator<CharT>> class num_get | num_get<wchar_t,istreambuf_iterator<wchar_t,char_traits<wchar_t>> |
議論のcodecvtを利用するファイルストリームはこの特殊化を必要とする。ファイルストリームと入出力するワイド文字列も特殊化が必要で通常のwstringを用いることができない。ファセットのいくつか(num_get、num_put、money_get、money_put、time_get、time_put)は第二仮引数にデフォルトで与えるイテレータがTraitsを持ち、これらも特殊化を必要とする。特殊化したファセットはlocaleインスタンスに追加しなければならない。コーディングの具体例はソースコード(自作のcodecvtファセット)の動作確認に示した。まとめれば様々なクラステンプレートの特殊化が必要で、basic_stringさえも特殊化が必要で、ファイルストリーム利用でwstringへのシフト演算子による入出力ができない。特殊化したbasic_strignとwstringの相互変換はc_strメンバ関数を必要とする。
ライブラリ標準codecvtファセットの置き換え(ライブラリ利用スキーム2)
ライブラリ利用スキーム2はcodecvt<wchar_t,char,mbstate_t>を派生クラスに置き換える(TMyCodeCvtクラステンプレート)。std名前空間外で定義、idスタティックメンバ変数は基底クラスのままで良い。Stateはデフォルトmbstate_tで、規格は各メンバ関数の第一引数に参照渡しされるmbstate_t参照に文字コードの変換状態を記憶するものとする(24.2.3.1/p4、24.2.3.4/p3)。しかしmbstate_tは実装依存型でmingw-w64は単にintにtypedefして状態記憶にとても使えない。変換状態は二つある事に改めて注意しよう。第一はステートフルな文字コードのエスケープシーケンスによる選択情報で、JISであればASCIIあるいはJIS漢字(JIS X 0208)のどちらかであり、この程度なら容易にintへ格納できる。第二は部分符号シーケンスで全て(例えばUTF-8の4符号長文字の1~3バイト)を記憶する必要があるが、こちらをintに格納するのは容易でない。なお、ソースコード(自作のcodecvtファセット)に示した部分変換スキーム2を採れば第二を記憶する必要は無い。ライブラリ利用スキーム2はTMyCodeCvtクラステンプレートの第一仮引数(myStateT)にTMyCodeCvtStateXXXクラスを与えて実体化する(TMyCodeCvtクラス)。TMyCodeCvtクラスを構築して変換状態をTMyCodeCvtStateインスタンスとなるメンバ変数(myState_)に格納する。
TMyCodeCvtクラステンプレート(TMyCodeCvt.h)
template<typename myStateT,typename stateT=mbstate_t> class TMyCodeCvt
:public std::codecvt<typename myStateT::intern_type,typename myStateT::extern_type,stateT>
{
public:
using result=std::codecvt_base::result;
using intern_type=typename myStateT::intern_type;
using extern_type=typename myStateT::extern_type;
using state_type=stateT;
private:
myStateT myState_;
public:
explicit TMyCodeCvt(size_t refs=0)
:std::codecvt<intern_type,extern_type,state_type>{refs},myState_{}
{}
protected:
~TMyCodeCvt() override {}
result do_in(state_type& state
,const extern_type* from,const extern_type* from_end,const extern_type*& from_next
,intern_type* to,intern_type* to_end,intern_type*& to_next) const override
{return myState_.do_in(from,from_end,from_next,to,to_end,to_next);}
result do_out(state_type& state
,const intern_type* from,const intern_type* from_end,const intern_type*& from_next
,extern_type* to,extern_type* to_end,extern_type*& to_next) const override
{return myState_.do_out(from,from_end,from_next,to,to_end,to_next);}
result do_unshift(state_type& state
,extern_type* to,extern_type* to_end,extern_type*& to_next) const override
{return myState_.do_unshift(to,to_end,to_next);}
int do_length(state_type& state
,const extern_type* from,const extern_type* from_end,size_t max) const override
{return myState_.do_length(from,from_end,max);}
bool do_always_noconv() const noexcept override {return myState_.do_always_noconv();}
int do_max_length() const noexcept override {return myState_.do_max_length();}
int do_encoding() const noexcept override {return myState_.do_encoding();}
};
メリット
TMyCodeCvtクラスがTMyCodeCvtStateXXXクラスを利用するのはスキーム1とのコード共通が理由で本質ではない。TMyCodeCvtStateXXXクラス利用で文字コード定義をテンプレート引数で与えざるを得ないが、実装をTMyCodeCvtクラス内に展開して文字コード定義をコンストラクタ引数とすることは容易だし、むしろ実用はこちらの方が自然だろう。つまりスキーム2であれば文字コードを実行時動的設定とすることが可能で、これが第一のメリットである。第二にライブラリ標準codecvt派生なのでファイルストリーム利用でシフト演算子による入出力が普通に行える。
デメリット
デメリットは大きく、codecvtファセット要件を逸脱する。codecvtファセットのパブリックメンバ関数は全てconstで内部状態の変更を意図しない。言い換えれば、規格は変換状態をメンバ変数に格納するスキーム2を想定しない(これを明確に規定するものは見当たらないが)。ところがTMyCodeCvtクラスのconstメンバ関数は所有するTMyCodeCvtStateXXXインスタンスのconstメンバ関数をコールし、そのconstメンバ関数がTMyCodeCvtStateXXXの内部状態を変更する。C++でこういった場合コンパイルエラーとなるべきだがなぜかそうならない。すなわち統語論(シンタックス)的に要件を逸脱しないと見なされる。以下にその理由を示す。
iconvライブラリのインターフェースがC言語で、変換記述子(iconv_t型でtypedefされたvoid*)を自由関数であるライブラリ関数の第一実引数に与えて操作する。ICUライブラリも同じく、変換オブジェクト(UConverter*型)を自由関数であるライブラリ関数の第一実引数に与える。すなわち変換記述子あるいは変換オブジェクトが内部状態を保持する"何か"を参照する。なおウィンドウズAPIは内部状態を全く保持しない。変換記述子あるいは変換オブジェクトはコンストラクタ/デストラクタで開いて/閉じてメンバ変数に保持し、ライブラリ関数をconstに縛られずコールできてしまう。
しかしそういった関数コールは変換記述子あるいは変換オブジェクトの参照する"何か"を変更するため、意味論(セマンティクス)的にcodecvtファセット要件を逸脱すると扱わなければならない。このデメリットは利用方法に大きな制約を加える。スキーム1であれば変換状態をcodecvtファセット内部に持たず同一インスタンスを複数のファイルストリーム(あるいはwfilebuf)で共用できるが、スキーム2は共用できない。ところでlocaleインスタンスは所有ファセットをポインタで保持し、そのコピーは参照カウントを増加してファセットを共有する(25.3.1.1.2/p3)。すなわち、あるファイルストリーム(あるいはwfilebuf)のlocaleインスタンスがスキーム2のcodecvtファセットを所有していて、そのコピーを他のファイルストリーム(あるいはwfilebuf)にimbueするとエラーとなる公算が大きい。つまり、スキーム2のcodecvtファセットを所有するlocaleインスタンスをコピーしてはならない。