ホーム < ゲームつくろー! < C++踏み込み編


その12 printfな可変長引数で文字列を返す関数


 printf(sprintf)は可変長の引数を取って文字列をコンソールに出力する(文字列を生成する)便利なC言語の関数です。C++になってstd::stringが使えるようになり、文字列の操作は随分と楽になりましたが、ちょっと面倒だなぁと感じることもあります。例えば、printfのように手軽に文字列を作りたいのですが、そのためには、

char c[1024];
sprintf( c, "Current Time is %d : %d : %d", hour, minute, second );
std::string str( c );

と一時変数を作る必要があります。ちょっと文字列を作る分には良いのですが、これが続くとだんだんと嫌気も出てきます。そこで、例えばこんな関数があると便利では無いでしょうか?

std::string str( printfString("Current Time is %d : %d : %d", hour, minute, second) );

この関数はprintfのような引数を取り、戻り値として文字列を返します。よってstd::stringの引数にそのまま渡す事ができます。1行で文字列を作れるのでとっても便利です。可変長引数を取る関数の実装は例えばこうなります:

#include <stdio.h>
#include <stdarg.h>
#include <tchar.h>

const TCHAR* printfString( const TCHAR* format, ... ) {
    static TCHAR strBuffer_g[1024];
    va_list args;
    va_start( args, format );

#if _DEBUG
    int len = _vsctprintf( format, args );
    if ( len >= 1024 )
        _ASSERT(0);
#endif

    _vstprintf( strBuffer_g, format, args );
    return strBuffer_g;
}

staticとして内部に1024バイトの共通文字列格納変数を用意しておきます。va_listやva_startはstdarg.h内で定義されているマクロで、可変長引数を扱うときのお約束です。_vsctprintf関数はtchar.hに定義されていて、可変長引数によって生成される書式付き文字列の文字数を返してくれます。上の実装例ではデバッグ時のみ1024バイトを超えていたらアサートで止めるようにしています。_vstprintf関数は可変長引数による書式付き文字列を作ってくれる関数です。

 これで先のstringに直接引き渡せる文字列を返す事ができます。上の例で1024文字に限定しているのは、ちょっとしたところで便利に使いたいためです。引数の文字数が不定の場合は上のような実装ではなくて、ちゃんと文字数をチェックする実装にすべきです。


 上の関数の応用の一つとして、C++ではないのですが、Direct3DにあるID3DXFont::DrawText関数を可変長引数にできるというのがあります。ID3DXFont::DrawTextは普通こういう使い方をします:

pFont->DrawText( NULL, "Current time is 20:48:37", -1, &rect, DT_LEFT  | DT_SINGLELINE, 0xffffffff );

第2引数に終端文字付きの文字列を渡すと指定の場所に文字列を描画してくれます。これはこれで良く出来ているのですが、いかんせん第2引数が固定文字列のため、事前にこの文字列を作っておく必要があり、微妙に使いにくいものでした。そこで先ほどの関数を適用すると可変長扱いができます:

pFont->DrawText( NULL, printfString("Current time is %d:%d:%d", hour, minute, second), -1, &rect, DT_LEFT  | DT_SINGLELINE, 0xffffffff );

ちょっとしたことなのですが、書式付きID3DXFontのような感覚になるためとても便利になります(^-^)


 ただ、printf系の可変長引数にはリスクが常に付いて回ります。引数の数が異なったり、書式付き文字列の「%*」と引数の型に不一致があったりするとメモリ破壊を含む深刻なエラーが起こる可能性があります。そこで、上の「可変長な文字列を1行で作れる」という理想を保ちつつ、より安全な方法を考えてみました。



@ std::string、std::stringstream、両者とも惜しいけど・・・

 文字列をより安全に扱う方法の一つがstd::stringです。これの良い所は、例えば、

std::string str1 = "This is a";
str1 += "pen.";

のように、演算子によって文字列を代入したり結合できる点にあります。また、

printf( str1.c_str() );

とstd::string::c_strメソッドを使うとconst char*型を返してくれるのでprintf等でも活用できます。これ理想の一つ「1行でconst char*型を返す」です。

 しかし、std::stringは文字列以外を扱うのが苦手です。例えば、

std::string str = 100.25f;  // error!

という代入はできません。よって、std::stringだけで可変長に色々な文字や値を要り混ぜて・・・という事はちょっと無理です。

 さて一方標準テンプレートライブラリにはstd::stringstreamという文字列をストリームのように扱うクラスが用意されています。「ストリームのように」というのは例えばこういう事です:

std::stringstream ss;
ss << "This is a" << "pen.";
std::string str = ss.str();

 左シフト演算子<<をオーバーロードし、まるで文字列を左側の「ss」に流し込んでいる(stream: 流れ)かのように見せています。この演算子は基本型とstd::stringであればいくらでも繋げられます。例えば、

std::stringstream ss;
int a = 100;
double b = 200.25;
std::string s = " This is a";
const char* c = "pen.";

ss << a << "---" << b << s << c;
std::string str = ss.str();

とすると、「100---200.25 This is a pen.」と数値や文字列を織り交ぜた可変長の文字列を作る事ができます。これは理想の一つ「可変長」を満たしています。

 しかし、とても残念なのですが1行で文字列を返す事ができません:

printf( (std::stringstream() << "Now" << hour << ":" << minute << ":" << second).str().c_str() )


 std::stringは色々な型が混じった可変長文字列を作りにくい、std::stringstreamは1行で文字列を返すのが難しい、両者惜しい所で理想をはずしています。なんとか両者のいいとこ取りができないか・・・、そこで作ったクラスがDix::Strです。



A Dix::Strクラス

 Dix::Strクラス(Dixというのは私がいつもつけている名前空間です)はstd::stringのような演算子による文字列の連結とstd::stringstreamのような左シフト演算子「<<」による可変長文字列の生成、そして理想である「1行でprintfが認識できるconst char*文字列を返す」を機能的に実現したクラスです。

 全実装は次の通りです:

#include <string>
#include <sstream>

namespace Dix {
    class Str {
        std::string s;

    public:
        Str() {}
        ~Str() {}

        template< class T >
        Str( const T &src ) {
            std::stringstream ss;
            ss << src;
            s = ss.str();
        }

        Str &operator << ( const Str &src ) {
            s += src.s;
            return *this;
        }

        template< class T >
        Str &operator << ( const T &src ) {
            std::stringstream ss;
            ss << src;
            s += ss.str();
            return *this;
        }

        operator const char* () { return s.c_str();}
        operator std::string() { return s;}
        Str &operator += ( const Str &src ) {
            s += src.s;
            return *this;
        }

        Str operator + ( const Str &src ) const {
            return Str(s + src.s);
        }

        const std::string &str() { return s; }
    };
}

内部にstd::stringを持っていてその演算子による連結機能を利用して文字列を作っています。std::stringの強化版のような実装です。コンストラクタはテンプレートになっていて、内部でstd::stringstreamの左シフト演算子により引数の型を文字列に変換しています。後は演算子のオーバーロードです。

 左シフト演算子を2つ用意しています。1つは右辺にDix::Str型を取る<<演算子です。これにより「ストリーム連結」が可能になるんです。どういうことかというと、

Dix::Str str1("This is a");
Dix::Str str2("pen.");
Dix::Str str;

str << str1 << str2;

とした時に、strは「This is a pen.」という連結した文字列を持つ事になります。これは演算子の演算順番により、まず

(str << str1) << str2;

と括弧でくくった部分が処理され、結果としてstrの中に「This is a」という文字列が格納されます。括弧の演算の結果「*this」つまりstr自身が返るので、上のコードは、

str << str2;

となります。後は同様にstr2がstrの中に追加で入るので、strは「This is a pen.」になるわけです。

 左シフト演算子<<はもう一つテンプレートであらゆる型(処理できる型)を右辺に取れるようにしています。T型の引数をstd::stringstreamの左シフト演算に投げ文字列化してstd::stringに連結しています。これにより、

str << str1 << 100 << 200.25 << "Oh!" << str2;

という文字列や数値、小数点などstd::stringstreamが認識できる物は連結してstrに格納されます!

 次に「operator const char*」という型のオーバーロードをしています。これは何かと言うと、Dix::Strをconst char*型として認識させているんです。内部ではstd::string::c_str()メソッドで返されるconst char*を返しているだけです。これにより、

Dix::Str str("This is a pen.");
printf( str );

とprintfに直接放り込めるんです!同様にstd::string型としても認識できるようにしたので、

Dix::Str str("This is a pen.");
std::string s = str;

という代入も問題ありません。

 +演算子及び+=演算子は文字列の連結をサポートするヘルパーです。こういう使い方ができます:

Dix::Str str1("This is a pen. "), str2("The price is "), str3(100), str4(" yen.");
str1 += str2 + str3 + str4;
printf( str1 );

これで「This is a pen. The price is 100 yen.」という文字列が出力できます。std::stringの演算子と同じ感覚で使えますね(^-^)。



B 1行で書式付き可変長文字列を安全に作って返す!

 Dix::Strクラスを使うと本章の「書式付き可変長文字列を1行で作ってconst char*型として返す」という目標を次のように達成できます:

int hour = 17, minute = 42, second = 39;
printf( Dix::Str() << "Current time is " << hour << ":" << minute << ":" << second << ".\n" );

最大のポイントはDix::Str()という空の一時変数を明示的に作ってしまう所にあります。その一時変数に対して左シフト演算子で文字列をどんどん流し込みます。最終的に出来た一時変数はoperator const char*の作用でconst char*型として認識されるため、printfに直接代入できるというカラクリです。

 同じ事はもちろんID3DXFont::DrawTextメソッドでも適用できます:

int hour = 17, minute = 42, second = 39;
pFont->DrawText(
    NULL,
    Dix::Str() << "Current time is " << hour << ":" << minute << ":" << second << ".\n",
     -1, &rect, DT_LEFT  | DT_SINGLELINE, 0xffffffff );

冒頭の可変長文字列の時のように文字数制限や引数間違いによるリスクはありません。他にもデバッグウィンドウに文字列を表示するOutputDebugString関数にも直接渡せます:

OutputDebugStringA( Dix::Str() << "Runtime Error: " << hour << ":" << minute << ":" << second << "\n" );

 本章に掲載されているDix::Strはツール編その7「マルペケ謹製文字列ストリームクラス」にて公開します。ツール編のDix::StrはUnicodeにも対応し、演算子の機能も充実していますので是非ご使用下さい(^-^)