lexical_castと大域ロケール

lexical_castが流行っているみたいなので、飛行機の中でコードを読んでました。
素朴な実装だとstringstreamに挿入(<<)して抽出(>>)するだけですが、
既定でstringstreamに大域ロケールが吹き込まれるため大域ロケールの影響を受けて出力が変わってしまいます。
数値の入出力に影響する特性項目(facet)にはnum_get、num_put、numpunctがありますが、
Boostの実装では一部の型に対して高速な実装を用意しているため、numpunctしか効きません。
ダミーfacetを使って実際に試してみました。

#include <locale>

// デバッグ用ダミーnumpunct
class debug_numpunct : public std::numpunct<char>
{
private:
    typedef std::numpunct<char> base_type;

public:
    typedef char char_type;
    typedef base_type::string_type string_type;

    explicit debug_numpunct(std::size_t ref=0) : base_type(ref)
    {
    }

protected:
    // 「.」→「:」
    char_type do_decimal_point() const // virtual
    {
        return ':';
    }

    // 「,」→「;」
    char_type do_thousands_sep() const // virtual
    {
        return ';';
    }

    // 一桁毎に区切る
    std::string do_grouping() const // virtual
    {
        return "\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1";
    }

    string_type truename() const // virtual
    {
        return "true(debug_numpunct)";
    }

    string_type falsename() const // virtual
    {
        return "false(debug_numpunct)";
    }
};

#include <sstream>

// 手抜きlexical_cast
template<class T, class U>
T lexical_cast(U x)
{
    std::stringstream ss;
    ss << x;

    T y;
    ss >> y;
    return y;
}

#include <hamigaki/debug_facets.hpp> // debug_num_get/debug_num_put
#include <boost/lexical_cast.hpp>
#include <iostream>

void lexical_cast_test()
{
    std::cout << boost::lexical_cast<std::string>(123456789) << std::endl;
    std::cout << lexical_cast<std::string>(123456789) << std::endl;
    std::cout << 123456789 << std::endl;
}

int main()
{
    try
    {
        // クラシック("C")ロケールで出力
        lexical_cast_test();

        // グローバルロケールにダミーロケールを設定して出力
        std::locale loc0(std::locale(), new hamigaki::debug_num_get);
        std::locale loc1(loc0, new hamigaki::debug_num_put);
        std::locale loc(loc1, new debug_numpunct);
        std::locale::global(loc);
        lexical_cast_test();

        // ダミーロケールで出力
        std::cout.imbue(loc);
        lexical_cast_test();

        // 元に戻しておく
        std::cout.imbue(std::locale::classic());
        std::locale::global(std::locale::classic());
    }
    catch (const std::exception& e)
    {
        std::cout << e.what() << std::endl;
    }
}

出力結果

123456789
123456789
123456789
1;2;3;4;5;6;7;8;9
1;2;3;4;5;6;7;8;9(debug_num_put)
123456789
1;2;3;4;5;6;7;8;9
1;2;3;4;5;6;7;8;9(debug_num_put)
1;2;3;4;5;6;7;8;9(debug_num_put)

このようにロケールの設定如何で出力が変わってしまうので、lexical_castの利用には注意が必要です。
大域ロケールをstd::locale::classic()に設定することで「123456789」のような出力を強制できますが、
全スレッド共通の設定なのでマルチスレッド環境ではよろしくないです。


こう書くと「atoi()/printf()最強」とか言われそうですが、
atoi()はロケールによる拡張を許可していますし、
printf()もPOSIX拡張では小数点が切り替わるようになっています。
どれを使うにしてもマニュアル/規格で確認した方がよいです。


ちなみに、std::numpunct::do_grouping()はstd::stringを整数の配列として利用しています。
std::stringがほぼCOW実装前提であった頃の名残かなぁと思います。