C++の罠

wchar_tを使うときの罠

日本語などのマルチバイト文字列を扱うソフトで、 正規表現などの文字列処理をもっと楽にやりたいという人は、 選択肢の一つとしてワイド文字列の使用を考えることでしょう。 最近では多くのライブラリがワイド文字列とシングルバイト文字列双方に対応し、 C言語も95年の改定でワイド文字列型wchar_tを導入し、 既存のchar型を使用する標準ライブラリはほぼ全てwchar_t版が用意されました。

無論C++も、98年にISOで標準化された際には、 文字列クラスはちゃんとstringとwstringの双方が用意されました。

ところが、実際にwchar_tを使用するとなると、様々な障壁が立ちはだかります。 ここでは、私が陥ったwchar_tに関する「罠」を紹介していきます。

罠その1:Win9xで使えない

WinNT系列では、文字列を扱う全てのAPIにchar版とwchar_t版 (それぞれ関数名の末尾にA, Wがついている)が用意されていますので、 何ら問題なくプログラミングを行うことができます。 ところが、Win9x系列の場合、ワイド文字列に対応しているAPIは、 文字コード変換関連のAPIと、DrawText関数ぐらいのもので(ちゃんと調べたわけではないので、他にもあるかもしれません)、 あとは全部char版のみなのです。

つまり。Win9xに対応したソフトで内部処理にwchar_tを使用しようと思った場合、 APIを呼び出すたびに、決して軽くない文字コード変換というペナルティを受けることになります。 Win9x系を捨ててしまうのも一つの手ですが……。

罠その2:wchar_tが使えない標準ライブラリ

昔私は、C/C++の標準ライブラリは全てwchar_tに対応していると思っていました。 でもよく調べてみると、ある3つの分野において、 char型にしか対応していない標準ライブラリがあることに気づきました。 具体的には、

の3つの分野です。

ロケール関係

具体的には、以下の関数において、char型のみの対応となっています。

私自身ロケールについては良くわからないので細かいことは書けないのですが、 例えば「wfstreamにデータを出し入れする」際や「char<=>wchar_tの変換を行う」際にはロケール情報が必要となります。 これらの処理は、いずれもワイド文字列を扱う上で必要となる初期化処理なので、 ワイド文字列を使う際には「char型を利用して初期化→wchar_t型を使う」という流れになるでしょう。

ファイルシステム関係

具体的には、以下の関数において、char型のみの対応となっています。

ファイルの中身を扱うfwrite/fread等の関数には、全てwchar_t版が用意されていますが、 肝心のファイルそのものを扱う関数類には何故かwchar_t版がありません。 一応、VCでWinNT系列だけという縛りを入れていいのならば、_wfopen()関数や_wrename()関数といったものが存在しますが、 それでもfstream系列は使うことができません。

boostのfilesystemもワイド文字列によるパス指定をサポートしていないんですよね…… なにか私の想像の及びもつかない理由があるのでしょうか。

例外関係

具体的には、以下の関数において、char型(std::string)のみの対応となっています。

例外関連のクラスは、全てchar型のみとなっています。 どういう深い理由があってこうなっているのかはわかりませんが、 例外を投げる際のエラーメッセージにワイド文字列を含めることができないというのは、結構不便に感じます。 もっとも、例外クラスは他のロケールクラスやファイルシステムクラスと違い、 自分で作るのが楽なので、自分でワイド文字列も扱えるような例外クラス書いてしまったほうがいいかもしれません。

罠その3:wchar_t = unsigned short ?

まずはじめに、次のコードをご覧ください。

001 #include <iostream>
002 
003 int main(void) {
004   std::wcout << L'a' << std::endl;
005   return 0;
006 }

'a'が出力されると思いますか? 少なくとも、VCでは出力されません。 このプログラムは、"97"を出力します。何故でしょうか?

C言語の遺産を受け継いでいるのかは知りませんが(Cではwchar_tはtypedefされた型です)、 wchar_tがunsigned shortのtypedefであるコンパイラが結構あります。 代表的なのがVCで、VC7以降ではwchar_tを組み込み型として使えるものの、 何故かデフォルトの設定ではunsigned shortのtypedefとなっています。 結果として、このようにストリームへの出力時にはwchar_tはunsigned shortとして扱われてしまうのです。

なので、当然こういったものもエラーになってしまいます。 VC7以降ならコンパイルオプションに/Zc:wchar_tを設定してやりましょう。

001 class hogehoge {
002   public:
003     void hoge(unsigned short i);
004     void hoge(wchar_t c);
005 };

こんなクラス定義があると、VC7.1ではこんなエラーを吐いてくれます。

error C2535: 'void hogehoge::hoge(unsigned short)' : メンバ関数は、既に定義または宣言されています。
'hogehoge::hoge' の宣言を確認してください。

wchar_tとunsigned shortが同じ型であることを知らないと、このエラーで苦労する羽目になります。 これも、wchar_tとunsigned shortが内部的に同一の型であるために生じるエラーです。

罠その4:結局wchar_tって内部表現どうなのよ?

仕様書には、これだけしか書いてありません。(ドラフト案より抜粋)

3.9.1 Fundamental types
5
Type wchar_t is a distinct type whose values can represent distinct codes for all members of the largest extended character set specified among the supported locales. Type wchar_t shall have the same size, signedness, and alignment requirements as one of the other integral types, called its underlying type.

wchar_tについて述べられていますが、見てのとおり、 具体的な文字コードやサイズについては何も記述されていません。 型については具体的な数値を出さないのがC以来の伝統ですので (99年の改定でその伝統もついに崩れましたが)、仕方ないかなという気もしますけれど。

wchar_tの内部表現の違いは、かなり身近に転がっています。 C++コンパイラの双璧(だと私は勝手に思っています)であるVCとgccで、扱いが異なるのです。 具体的には、VCではwchar_tは2バイトで、文字コードは「サロゲート・ペア部分を取り除いたBOMなしのUTF-16」、 gccではwchar_tは4バイトで、文字コードはUCS4です。

このようにコンパイラによって実装がまちまちなので、 ICU等のマルチプラットフォーム対応・Unicode使用のライブラリでは、 独自のワイド文字列型(ICUではUChar型)を定義して利用している場合が多いです。 甚だしく本末転倒だと思えるのは私だけでしょうか?

罠その5:出力できない?

VC+STLport4.6.2の環境でしか試していないのですが。

001 #include <iostream>
002 
003 int main(void) {
004   std::wcout << L"こんにちは、世界" << std::endl;
005   return 0;
006 }

このコードを実行すると、「S徒ao┌┬L」などという意味不明な出力を吐いてくれます。

これはロケールの設定をしていないせいだろうと気づいて、ロケールの設定をしてみます。

001 #include <iostream>
002 #include <locale>
003 
004 int main(void) {
005   std::locale::global(new std::locale("japanese"));
006   std::wcout << L"こんにちは、世界" << std::endl;
007   return 0;
008 }

しかし、やはり結果は同じです。意味不明な出力を吐くだけです。

どうやらSTLportでは、std::locale::global()関数は、「これから生成される」オブジェクトのロケールは設定してくれますが、 すでに生成済みのオブジェクトのロケールには影響を及ぼさないようです。 std::wcoutにロケールを設定するのは、次の方法が正解です。

001 #include <iostream>
002 #include <locale>
003 
004 int main(void) {
005   std::wcout.imbue(std::locale("japanese");
006   std::wcout << L"こんにちは、世界" << std::endl;
007   return 0;
008 }

std::basic_ios::imbue()関数で、ロケールの設定ができます。これでなんとか、wcoutが使えるようになりました。

罠その6:読み込めない?

こちらは、もう少し深刻な問題です。以下のコードを実行してみましょう。

001 #include <string>
002 #include <iostream>
003 #include <locale>
004 
005 int main(void) {
006   std::wcin.imbue(std::locale("japanese"));
007   std::wcout.imbue(std::locale("japanese"));
008   std::wstring str;
009   
010   std::wcout << L"文字列を入力してください" << std::endl;
011   std::wcin >> str;
012   std::wcout << str << std::endl;
013   return 0;
014 }

入力をそのまま出力するだけのこれ以上ないほど単純なプログラムです。 いくつか、入力を試してみましょう。

文字列を入力してください
abcde
abcde

文字列を入力してください
日本語のセンテンス
日本語のセンテンス

文字列を入力してください
12 34
12

wistream::operator>>(std::wstring&)関数は、 スペースもしくはストリーム終端に達するまで読み込むのが仕様ですから、 3番目の結果も特に問題はありません。

一見正しく動いているように見えますが……(こういうのが一番怖いのです)、 こんな入力を試してみましょう。

文字列を入力してください
!”#$%&’()
 I h 煤吹刀普f i j

ご覧のとおり、全角記号は壊滅状態です。

解決方法見つけました。STLportのルートディレクトリを基準に、 src/c_locale_win32/c_locale_win32.c にあるファイルの352行めを、

352 for(i=*ptr; i <= *(ptr+1); i++) ltype->ctable[i+1] = _LEADBYTE;
    // ↓
352 for(i=*ptr; i <= *(ptr+1); i++) ltype->ctable[i] = _LEADBYTE;

のように変更すればこのバグは修正されます。

最新版(5.0系列)では修正されているらしいですね。 試してないですが。

おまけ:std::string<->std::wstring間の変換プログラム

ここに。