VC8のfilebufとsetlocale

今日はライブラリの作業を進める気分ではなかったので、前々から気になっていた、VC8のstd::filebufがstd::setlocale()を呼ばないと動かない問題と、std::setlocale()を呼ぶと標準出力/エラー出力がおかしくなる問題について調べてみました。


原因に関してはこんな感じでした。

std::filebufのファイル名がCのロケールに従ってワイド文字に変換される

Cのロケールを変更するためにstd::setlocale()を呼ぶことになる

Cのロケールが"C"以外になると、ファイルポインタへの書き込みで、ワイド文字経由でコンソールのコードページへの変換が発生する

既定ではstdoutやstderrがバッファリングなしの設定なので、マルチバイト文字のリーディングバイトだけでワイド文字に変換しようとして失敗する

書き込みに失敗したため、std::filebufがbad状態になり、その後の書き込みがすべて失敗する


対処法ですが、移植性を気にしなくてよければstd::filebufのワイド文字列版コンストラクタを使い、CロケールC++グローバルロケールは変更しないのが簡単です。
この場合、個々のストリームのロケールは自由に変更して問題ありません。


ワイド文字列版を使いたくなければ、CストリームかC++ストリームのどちらかにバッファを設定しないといけません。
例えば、こんな感じになります。

#include <cassert>
#include <clocale>
#include <fstream>
#include <iostream>
#include <locale>
#include <vector>

int main()
{
    typedef std::codecvt<wchar_t,char,std::mbstate_t> cvt_type;

    std::locale loc("");
    const cvt_type& cvt = std::use_facet<cvt_type>(loc);

    // 多分、このサイズで大丈夫
    std::vector<char> cout_buf(cvt.max_length());
    std::cout.rdbuf()->pubsetbuf(&cout_buf[0], cout_buf.size());

    // ofstreamのため必要
    std::setlocale(LC_ALL, "");

    std::ofstream os("日本語.txt");
    assert(!!os);

    // コンソールのコードページと同じなので"C"のままでも書ける
    std::cout << std::cout.rdbuf()->getloc().name() << std::endl;
    std::cout << "こんにちは、世界!" << std::endl;

    // ロケールを設定してもOK
    std::cout.imbue(loc);
    std::cout << std::cout.rdbuf()->getloc().name() << std::endl;
    std::cout << "こんにちは、世界!" << std::endl;
    std::cout.imbue(std::locale::classic());

    std::setlocale(LC_ALL, "C");

    // 念のため、バッファを戻しておく
    std::cout.rdbuf()->pubsetbuf(0, 0);
}

出力結果はこうなりました。

C
こんにちは、世界!
Japanese_Japan.932
こんにちは、世界!


まだ、ワイド文字絡みで曖昧な部分が残っていますが、これまで謎だった部分は大体解明できました。
これがVC8のバグかと言われると微妙な気がしますね。