エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

書式付き出力

某所でC言語printfの書式指定って初学者にはけっこうめんどうだよね、というブログのエントリを見て。


たとえば。C言語だとこんな感じ。

#include <stdio.h>

int main(int argc, char* argv[])
{
    const int    n   = 123;
    const double r   = 123.45;
    const char   s[] = "hoge";

    printf("n = [%10d]\n", n);
    printf("n = [%-10d]\n", n);
    printf("r = [%10f]\n", r);
    printf("r = [%-10f]\n", r);
    printf("r = [%10.3f]\n", r);
    printf("r = [%-10.3f]\n", r);
    printf("s = [%10s]\n", s);
    printf("s = [%-10s]\n", s);

    return 0;
}


実行結果。

n = [       123]
n = [123       ]
r = [123.450000]
r = [123.450000]
r = [   123.450]
r = [123.450   ]
s = [      hoge]
s = [hoge      ]

もっとも、C++のストリームで書式付きで出力するのはめんどうだ、という話のほうがよく聞きます。なのでC++でもprintfを使っている場面を見かけます。


たとえば。C++だとこんな感じ。実行結果は上と同じなので省略。

#include <iostream>
#include <iomanip>

int main(int, char* [])
{
    using namespace std;

    const int    n   = 123;
    const double r   = 123.45;
    const char   s[] = "hoge";

    cout << "n = [" << right << setw(10) << n << "]" << endl;
    cout << "n = [" << left  << setw(10) << n << "]" << endl;
    cout << "r = [" << right << setw(10) << r << "]" << endl;
    cout << "r = [" << left  << setw(10) << r << "]" << endl;
    cout << fixed << setprecision(3); // 表示範囲を小数点以下3桁に固定
    cout << "r = [" << right << setw(10) << r << "]" << endl;
    cout << "r = [" << left  << setw(10) << r << "]" << endl;
    cout << "s = [" << right << setw(10) << s << "]" << endl;
    cout << "s = [" << left  << setw(10) << s << "]" << endl;

    return 0;
}


これをもう少し簡単にできないかと考えてみた。
書式指定の仕様を全部やると大変なので、ここでやるのは幅、小数点以下桁数、位置(右寄せ/左寄せ)だけですが。

マニピュレータを自作して解決してみる

オーソドックスにマニピュレータを自作。
setformat関数でパラメータをForamtのオブジェクトに保存し、そのオブジェクトがストリームに出力されるときに一度にパラメータを設定しています。右寄せ左寄せは幅の値の正負で判定しています。

#include <iostream>

struct Format
{
    const int width;
    const int precision;
};

std::ostream& operator << (std::ostream& out, const Format& format)
{
    if(format.precision > 0)
    {
        out << std::fixed;
        out.precision(format.precision);
    }
    if(format.width > 0)
    {
        out << std::right;
        out.width(format.width);
    }
    else
    {
        out << std::left;
        out.width(-format.width);
    }
    return out;
}

Format setformat(int width, int precision = 0)
{
    Format format = { width, precision };
    return format;
}

int main(int, char* [])
{
    using namespace std;

    const int    n   = 123;
    const double r   = 123.45;
    const char   s[] = "hoge";

    cout << "n = [" << setformat( 10)    << n << "]" << endl;
    cout << "n = [" << setformat(-10)    << n << "]" << endl;
    cout << "r = [" << setformat( 10)    << r << "]" << endl;
    cout << "r = [" << setformat(-10)    << r << "]" << endl;
    cout << "r = [" << setformat( 10, 3) << r << "]" << endl;
    cout << "r = [" << setformat(-10, 3) << r << "]" << endl;
    cout << "s = [" << setformat( 10)    << s << "]" << endl;
    cout << "s = [" << setformat(-10)    << s << "]" << endl;

    return 0;
}

テンプレートにしてみる

printfで指定される書式はリテラルで指定される場合がほとんど。上記でいえばマニピュレータに渡される値は定数になるということ。
ならテンプレートでもいいんじゃね?ということでテンプレートにしてみる。

#include <iostream>

template<int W, int P>
std::ostream& setformat(std::ostream& out)
{
    if(P > 0)
    {
        out << std::fixed;
        out.precision(P);
    }
    if(W > 0)
    {
        out << std::right;
        out.width(W);
    }
    else
    {
        out << std::left;
        out.width(-W);
    }
    return out;
}

template<int W>
std::ostream& setformat(std::ostream& out)
{
    if(W > 0)
    {
        out << std::right;
        out.width(W);
    }
    else
    {
        out << std::left;
        out.width(-W);
    }
    return out;
}

int main(int, char* [])
{
    using namespace std;

    const int    n   = 123;
    const double r   = 123.45;
    const char   s[] = "hoge";

    cout << "n = [" << setformat< 10>    << n << "]" << endl;
    cout << "n = [" << setformat<-10>    << n << "]" << endl;
    cout << "r = [" << setformat< 10>    << r << "]" << endl;
    cout << "r = [" << setformat<-10>    << r << "]" << endl;
    cout << "r = [" << setformat< 10, 3> << r << "]" << endl;
    cout << "r = [" << setformat<-10, 3> << r << "]" << endl;
    cout << "s = [" << setformat< 10>    << s << "]" << endl;
    cout << "s = [" << setformat<-10>    << s << "]" << endl;

    return 0;
}

補足。実際にマニピュレータを使う場面で、関数の呼び出しでなく関数ポインタをストリームに送るということをしています。どういう動きになっているのかはここで解説されています(endlの下り)。こんなこと、これ読むまで知りませんでしたヨ。

条件分岐もなくせね?

パラメータが定数なら、どの条件で使われるかはコンパイル時にわかるので、条件分岐もなくせるはず。ということでなくしてみた。ちなみに、メンバ関数になっているのは、関数テンプレートでは部分特殊化ができないからというのがその理由。

#include <iostream>

template<int W, bool WF, int P, bool PF> struct Format;

template<int W, int P>
struct Format<W, true, P, true>
{
    static std::ostream& setformat(std::ostream& out)
    {
        out << std::fixed;
        out.precision(P);
        out << std::right;
        out.width(W);
        return out;
    }
};

template<int W, int P>
struct Format<W, false, P, true>
{
    static std::ostream& setformat(std::ostream& out)
    {
        out << std::fixed;
        out.precision(P);
        out << std::left;
        out.width(-W);
        return out;
    }
};

template<int W, int P>
struct Format<W, true, P, false>
{
    static std::ostream& setformat(std::ostream& out)
    {
        out << std::right;
        out.width(W);
        return out;
    }
};

template<int W, int P>
struct Format<W, false, P, false>
{
    static std::ostream& setformat(std::ostream& out)
    {
        out << std::left;
        out.width(-W);
        return out;
    }
};

template<int W, int P>
std::ostream& setformat(std::ostream& out)
{
    return Format<W, (W > 0), P, (P > 0)>::setformat(out);
}

template<int W>
std::ostream& setformat(std::ostream& out)
{
    return Format<W, (W > 0), 0, false>::setformat(out);
}

int main(int, char* [])
{
    using namespace std;

    const int    n   = 123;
    const double r   = 123.45;
    const char   s[] = "hoge";

    cout << "n = [" << setformat< 10>    << n << "]" << endl;
    cout << "n = [" << setformat<-10>    << n << "]" << endl;
    cout << "r = [" << setformat< 10>    << r << "]" << endl;
    cout << "r = [" << setformat<-10>    << r << "]" << endl;
    cout << "r = [" << setformat< 10, 3> << r << "]" << endl;
    cout << "r = [" << setformat<-10, 3> << r << "]" << endl;
    cout << "s = [" << setformat< 10>    << s << "]" << endl;
    cout << "s = [" << setformat<-10>    << s << "]" << endl;

    return 0;
}