Into the Horizon

programming, photography, and daily log

c++で日本語の処理(ロケール周り) 7/8追記

またもや問題が発生したので…
この辺りのページが参考になりました。ありがとうございます。
http://0xc000013a.blog96.fc2.com/blog-entry-226.html
http://tkmakwins15.tuzikaze.com/contents/winapicpp4.htm


■問題

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <locale>
using namespace std;

void hoge(){
	cout << "aiueo " << endl;
	cout << 12345<<endl;
	cout << "あいうえお" << endl;
	wcout << L"waiueo" << endl;
	wcout << 12345<<endl;
	wcout << L"あいうえお" << endl;
}

void read(){
	wstring wstr;
	wifstream fin("hoge.txt");
	while(fin >> wstr){
		wcout << wstr << endl;
	}
}

int main (int argc, char * argv[])
{	
	setlocale( LC_CTYPE, "ja_JP.UTF-8");
	read();
	hoge();
}

以上のコードが、ローカル(Macのターミナル上)だと上手くいっていたのだが、研究室のサーバー上では以下のエラーが出た。

terminate called after throwing an instance of 'std::ios_base::failure'
  what():  basic_filebuf::underflow invalid byte sequence in file
Aborted

read()のwifstreamの部分でエラーを吐いてるよう。
ローカルのg++のバージョンは4.6.2、サーバー上だと4.5.2。バージョンの違いが問題?


■解決策1
setlocaleが怪しかったので、とりあえず以下のコードにしてみました。
一応上手くいったのですが、hoge()のcout部分が出力されなくなってしまった。
read()をコメントアウトするとcoutが出力されるけど、L"あいうえお"の出力がバグったりと、よくわからない挙動。
更に言えば、ローカルだと以下のコードでエラーが出る…なんぞ

void hoge(){
~~
}

void read(){
~~
}

int main (int argc, char * argv[])
{	
	//setlocale( LC_CTYPE, "ja_JP.UTF-8");
	std::locale::global(std::locale(""));
	
	read();
	hoge();
}


■解決策2
imbueを使ってやる必要があるらしい?以下、上の参考URLからの引用。

グローバルなロケールを設定する場合、std::localeクラスのstaticメンバ関数のglobalを用いて設定します。
std::locale std::locale::global(const std::locale&);

これにロケールクラスのインスタンスを引き渡します。ただし、このロケールが影響を与えるのは、これを実行した後で生成したインスタンスに対してだけです。
たとえば、std::wcoutには影響を及ぼしません。そこで、std::basic_ostreamクラスの基底クラスであるstd::iosクラスのimbueメンバ関数を呼びましょう。
std::ios::imbue(const std::locale&);

これにlocaleクラスのオブジェクトを渡せばいいです。
Cであれば、setlocale()関数によって設定します。これはプログラム全体に影響を与えます。ロケール文字列は同じです。
ちなみに、std::locale::global()を実行すると、setlocaleを呼び出したのと同じ状態になります。つまり、Cライブラリのために、改めて設定する必要はありません。(C++規格上は、ですが…)

しかし、単にimbueを使ってもうまく行かず…。
先輩や准教にお聞きしてみた結果、なにやら下のコードでうまくいくとのこと。

int main (int argc, char * argv[])
{	
	//setlocale( LC_CTYPE, "ja_JP.UTF-8");
	std::ios_base::sync_with_stdio(false);
	std::locale::global(std::locale("")); 
	std::wcout.imbue(std::locale(""));
	//std::wcout.imbue(std::locale("Japanese", std::locale::ctype)); //本当はこっち?
	std::wcin.imbue(std::locale(""));

	read();
	hoge();
}

sync_with_wtdio(false)が重要なようです。C++の入出力とCの入出力の同期をとるかどうかを設定しているみたいですが…え、関係あるの?

そして、この状態だとwcoutの出力がカンマ区切りになります。

	wcout << 12345<<endl; // 12,345と出力される

これはロケールをまとめて設定しているためで、本当はLC_CTYPE(文字処理関数)だけを設定する必要があります。
そのためのコードがコメントアウトしたほうのwcout.imbueなのですが…こちらを実行すると「no matching function for call」と怒られます。ぼぼぼ…

このあたりで、面倒になって放置しました。global設定してwcoutだけ使うことにすれば一応問題はないし。先輩達が分からないと言ってる時点で無理ゲーな気がする
ということで、どなたか知ってる方がいればお願いします。



■7/8追記
コメントを頂いたので追記します。ありがとうございます!
以下で上手くいきました。

using namespace std;

int main (int argc, char * argv[])
{		
	ios_base::sync_with_stdio(false);
	locale default_loc("");
	locale::global(default_loc);
	locale ctype_default(locale::classic(), default_loc, locale::ctype); //※
	wcout.imbue(ctype_default);
	wcin.imbue(ctype_default);
	// std::wcerrやstd::wclogにも必要に応じてimbueする
	
	read();
	hoge();
}

※の行のコンストラクタでは、「std::locale::classic()をベースとして、ただしctypeだけdefault_locとする」という意味になります。(コメントそのままですいません)
こうやって引数を渡してやればよかったのですね。なるほど。
コメントを頂いたegtra様、ありがとうございました。


■更に追記
ローカルとサーバーの環境の違いでエラーが出る件について詳しく書いていなかったので、もうちょっと詳しく書きます。
前回に記事を書いてからサーバー上のg++のバージョンを上げてしまったので、上で書いてることとちょっと違います(´・ω・`)
サーバー:Ubuntu 11.10のg++ 4.6.1
ローカル:Mac 10.7.3のg++ 4.6.2

まず、一番最初のコード(setlocaleのみした時)について。
ローカルだと問題なく動作します。
サーバー上だと、エラーは起きない(g++のバージョンが上がったため?)ものの、出力は理想と違います。

$ ./a.out
(hoge.txtの一行目のみ。二行目以降表示されず)
waiueo
12345
あいうえお

最後のコード(追記したもの)について。
サーバー上だと問題なく動作します。
ローカルだと

terminate called after throwing an instance of 'std::runtime_error'
  what():  locale::facet::_S_create_c_locale name not valid
Abort trap: 6

という例外がおきます。
以下のサイトでも同じ問題が書かれてたんですが、よく理解できませんでした(白目
http://d.hatena.ne.jp/whitypig/20101202/1291293537
http://d.hatena.ne.jp/eagletmt/20090208
とりあえず、setlocale使っとけ、という結論らしいです。

無知過ぎてやばい…