iostreamでカスタムフォーマット

Boost.FusionのカスタムIOフォーマットのようなことを実現する方法についてです。


Boost.Fusionのタプルは通常 "(a b c)"のようなフォーマットで入出力されますが、このフォーマットをユーザー側でカスタマイズできます。カスタマイズは、tuple_open()、tuple_close()、tuple_delimiter()をstreamのオブジェクトに流すことでできます。

#include <iostream>
#include <boost/fusion/include/make_vector.hpp>
#include <boost/fusion/include/io.hpp>

namespace fusion = boost::fusion;

int main()
{
    // 入出力フォーマットを(a b c)から[a, b, c]に変更
    std::cout << fusion::tuple_open('[');
    std::cout << fusion::tuple_close(']');
    std::cout << fusion::tuple_delimiter(", ");

    std::cout << fusion::make_vector(1, 'a', "Hello") << std::endl;
}
[1, a, Hello]

このカスタマイズ動作の裏では、iostreamのxalloc()、pword()というのが使われています。
Boost.Fusionのソースで言うと、boost/fusion/sequence/io/以下にあります。


今回は、標準の入出力フォーマットが定められていないstd::vectorを、ストリームに出力可能にし、フォーマットをカスタマイズできるようにしてみます。まず、全体のコードです。

#include <iostream>
#include <string>
#include <vector>

struct vector_open {};
struct vector_close {};
struct vector_delimiter {};

template <class Tag>
int vector_xalloc()
{
    static int index = std::ios::xalloc();
    return index;
}

template <class Tag, class Stream>
void attach(Stream& stream, const std::string& s)
{
    static std::string value;
    value = s;
    stream.pword(vector_xalloc<Tag>()) = &value;
}

template <class Tag, class Stream>
std::string* const get_attached_value(Stream& stream)
{
    return reinterpret_cast<std::string*>(stream.pword(vector_xalloc<Tag>()));
}

template <class Stream>
std::string get_open_val(Stream& stream)
{
    if (std::string* const open_val = get_attached_value<vector_open>(stream))
        return *open_val;
    else
        return "{";
}

template <class Stream>
std::string get_close_val(Stream& stream)
{
    if (std::string* const close_val = get_attached_value<vector_close>(stream))
        return *close_val;
    else
        return "}";
}

template <class Stream>
std::string get_delimiter_val(Stream& stream)
{
    if (std::string* const delim_val = get_attached_value<vector_delimiter>(stream))
        return *delim_val;
    else
        return ",";
}

template <class OutputStream, class T>
void output_vector(OutputStream& os, const std::vector<T>& v)
{
    os << get_open_val(os);

    bool first = true;
    for (const T& x : v) {
        if (first)
            first = false;
        else
            os << get_delimiter_val(os);

        os << x;
    }

    os << get_close_val(os);
}

int main()
{
    const std::vector<int> v = {1, 2, 3};
    output_vector(std::cout, v);

    std::cout << std::endl;

    attach<vector_open>(std::cout, "[");
    attach<vector_close>(std::cout, "]");
    attach<vector_delimiter>(std::cout, ", ");
    output_vector(std::cout, v);
}
{1,2,3}
[1, 2, 3]

出力結果の上がデフォルト、下がカスタムフォーマットです。


一つひとつ見ていきましょう。まず一番上のタグ。

struct vector_open {};
struct vector_close {};
struct vector_delimiter {};

これら3つの空クラスは、開きカッコ、閉じカッコ、区切り文字を識別するためのタグです。あとで使います。


次にvector_xalloc()です。

template <class Tag>
int vector_xalloc()
{
    static int index = std::ios::xalloc();
    return index;
}

std::ios::xalloc()関数は、呼ばれる度に新たなインデックスを生成して返します。一度だけ生成してほしいのでstatic変数にしています。
それと、この関数がテンプレートになっているのは、vector_open、vector_close、vector_delimiterの3つのタグに対してそれぞれ異なるインデックスを生成してほしいからです。テンプレート引数によって異なるstatic変数が生成されます。


次はattach()です。この関数で、カスタムフォーマットを登録します。

template <class Tag, class Stream>
void attach(Stream& stream, const std::string& s)
{
    static std::string value;
    value = s;
    stream.pword(vector_xalloc<Tag>()) = &value;
}

テンプレートパラメータTagのバリエーションごとに、カスタムフォーマット用staticオブジェクトを生成しています。
カスタムフォーマットオブジェクトへのポインタを、streamオブジェクトのpword()に登録します。pword()は、インデックス値を渡すと、それに対応した値へのvoid*&を返します。デフォルトでは対応する値がないのでNULLが返ります。このpword()の戻り値にポインタを渡すことでカスタムフォーマットが登録されます。


次にget_attached_value()です。この関数で、登録されたカスタムフォーマットを取得します。

template <class Tag, class Stream>
std::string* const get_attached_value(Stream& stream)
{
    return reinterpret_cast<std::string*>(stream.pword(vector_xalloc<Tag>()));
}

これは単にpword()を呼んで、各タグのインデックスに対応した値へのポインタを取得しているだけです。登録されているフォーマットがない場合はNULLが返ります。


これで下準備がすべて整いました。あとは使うだけです。


実際に出力を行うoutput_vector()関数は以下のようになっています:

template <class OutputStream, class T>
void output_vector(OutputStream& os, const std::vector<T>& v)
{
    os << get_open_val(os);

    bool first = true;
    for (const T& x : v) {
        if (first)
            first = false;
        else
            os << get_delimiter_val(os);

        os << x;
    }

    os << get_close_val(os);
}

ここでは、開きカッコ、閉じカッコ、区切り文字を文字列リテラルで直接出力しているのではなく、値を出得してから出力しています。開きカッコのところだけ見てみましょう。

template <class Stream>
std::string get_open_val(Stream& stream)
{
    if (std::string* const open_val = get_attached_value<vector_open>(stream))
        return *open_val;
    else
        return "{";
}

登録されたカスタムフォーマットを取得する関数get_attached_value()関数に、開きカッコを識別するためのvector_openタグを指定した上でstreamオブジェクトを渡しています。これで、pword()で登録されたカスタムフォーマットが取得されます。
カスタムフォーマットが登録されていたらそれを返し、そうでなければデフォルト値の"{"を返しています。
閉じカッコと区切り文字も同じです。


最後に、カスタムフォーマット登録のユーザーコードです。

const std::vector<int> v = {1, 2, 3};
output_vector(std::cout, v);

std::cout << std::endl;

attach<vector_open>(std::cout, "[");
attach<vector_close>(std::cout, "]");
attach<vector_delimiter>(std::cout, ", ");
output_vector(std::cout, v);

最初のoutput_vector()呼び出しはカスタムフォーマットが登録されていないので、pword()でNULLが返り、デフォルトフォーマットで出力されます。


attach()関数の開きカッコ、閉じカッコ、区切り文字のタグを指定した上でカスタムフォーマット文字列を渡すことで登録されます。
登録後にoutput_vector()を呼び出すことによって、pword()でカスタムフォーマットが返り、"[a, b, c]"というフォーマットで出力されます。


これで入出力フォーマットのカスタマイズができました。


おまけ。
Boost.IO State Saverを使うとカスタムフォーマットを戻せます。これは、attach()を使うユーザーコード側の対応だけでいけます。
ios_pword_saverはxalloc()のインデックス値をコンストラクタで受け取り、対応するpword()が指す値を覚えておき、デストラクタでpword()に代入し直してるだけですね。

#include <iostream>
#include <string>
#include <vector>
#include <boost/io/ios_state.hpp>

struct vector_open {};
struct vector_close {};
struct vector_delimiter {};

template <class Tag>
int vector_xalloc()
{
    static int index = std::ios::xalloc();
    return index;
}

template <class Tag, class Stream>
void attach(Stream& stream, const std::string& s)
{
    static std::string value;
    value = s;
    stream.pword(vector_xalloc<Tag>()) = &value;
}

template <class Tag, class Stream>
std::string* const get_attached_value(Stream& stream)
{
    return reinterpret_cast<std::string*>(stream.pword(vector_xalloc<Tag>()));
}

template <class Stream>
std::string get_open_val(Stream& stream)
{
    if (std::string* const open_val = get_attached_value<vector_open>(stream))
        return *open_val;
    else
        return "{";
}

template <class Stream>
std::string get_close_val(Stream& stream)
{
    if (std::string* const close_val = get_attached_value<vector_close>(stream))
        return *close_val;
    else
        return "}";
}

template <class Stream>
std::string get_delimiter_val(Stream& stream)
{
    if (std::string* const delim_val = get_attached_value<vector_delimiter>(stream))
        return *delim_val;
    else
        return ",";
}

template <class OutputStream, class T>
void output_vector(OutputStream& os, const std::vector<T>& v)
{
    os << get_open_val(os);

    bool first = true;
    for (const T& x : v) {
        if (first)
            first = false;
        else
            os << get_delimiter_val(os);

        os << x;
    }

    os << get_close_val(os);
}

int main()
{
    const std::vector<int> v = {1, 2, 3};
    output_vector(std::cout, v);

    std::cout << std::endl;

    {
        boost::io::ios_pword_saver open_saver(std::cout, vector_xalloc<vector_open>());
        boost::io::ios_pword_saver close_saver(std::cout, vector_xalloc<vector_close>());
        boost::io::ios_pword_saver delimiter_saver(std::cout, vector_xalloc<vector_delimiter>());

        attach<vector_open>(std::cout, "[");
        attach<vector_close>(std::cout, "]");
        attach<vector_delimiter>(std::cout, ", ");
        output_vector(std::cout, v);
    }

    // 元に戻っているか確認
    std::cout << std::endl;
    output_vector(std::cout, v);
}
{1,2,3}
[1, 2, 3]
{1,2,3}