C
C++
va_list
vprintf

va_list、可変長引数の仕組みを理解してvprintf関数を使う

More than 1 year has passed since last update.

printf関数一族の豪勢な変換機能を自前のプログラムに取り込みたい場面があったので、vprintf系関数をなんとか使うべく、可変長引数の仕組みを調べてみました。

環境

使っているのはwindowsでVisualStudioな環境です。
どこかにva_listの定義等はCPUなどで変わるって書いてあったので、とりあえず書いておきます。

可変長引数とは

字の通り、個数が定まっていない引数のことです。可変長引数を取る関数の代表選手はprintfです。
printfの定義は
int printf( const char *format, ... );
で、"..."の部分に任意個数の引数が書けます。
つまり、
printf( "引数2個 %d %d\n", 100, 200 );
printf( "引数4個 %d %d %d %d\n", 100, 200, 300, 400 );
という感じです。

可変長引数の関数は、あらかじめ渡したい引数の型・個数が決まっている場合は非常に便利なんですが、ダイナミックに個数・型が変わる場合は、通常使えません。矛盾する話のようですが、呼び出す側で引数の型個数をその都度変化させて呼び出す事は普通は出来ないからです。

可変長引数を1個の変数に変換する

自前で可変長引数処理を作る場合は、va_list型とそれを操作するマクロ群(va_start,va_arg,va_end)を使います。va_list型を使えば任意個の引数を1個の変数に変換してくれます。1個のva_list型変数から引数を次々取り出したり、vprintf系関数にまとめて渡したり出来ます。
例えば、printfと同じ動作をするmyPrintfはva_listとvprintfを使って次のように書けます。

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

void myPrintf( const char *format, ... )
{
    va_list ap;

    // 可変長引数を1個の変数にまとめる
    va_start( ap, format );
    // まとめられた変数で処理する
    vprintf( format, ap );
    va_end( ap );
    // 戻り値省略
}

va_listの中身

va_listは
typedef char* va_list
と定義されています。ただのchar型のポインタです。えーー、そんなんでええんか??
つまるところ、va_startマクロは可変長引数の列の最初の変数へのポインタをva_list型変数(char*型だ)に設定しているだけなのでした。
va_argマクロもまた、ポインタを進めていって、それぞれの引数値にアクセスするだけのものでした。

具体的には、先に例示したmyPrintfで
myPrintf( "%d %f %s\n", 100, 3.14, "piyo" );
を実行した場合、myPrtintfの中では、va_list型の変数apにva_startマクロによって以下のポインタが設定されます。

ポインタ apからのオフセット  型(サイズ)    
ap-> 0 100 int型(4バイト)
4 3.14 double型(8バイト)
12 "piyo"を指すポインタ char*型(4バイト:32bitの場合)

vprintf関数では、フォーマット文字列で型を認識しながらこのポインタapを辿って数値を表示していると想像できます。こんな感じです。

フォーマット文字列は"%d %f %s\n"
最初は、"%d" → int型、apからint型の100を取り出してapを4バイト進める
次は、"%f" → double型、apからdouble型の3.14を取り出してapを8バイト進める
最後は、"%s" → char*型、apからchar型のポインタを取り出してapを4バイト進める
おしまい。

独自にva_listを作成してvprintfを使う

独自にva_listを作って好きな型・個数の変数を仕込んでvprintfに渡してあげれば、printf関数一族の多彩なフォーマット変換機能を自由に使う事ができます。

va_listを独自に作って変数を詰め込むには、まず変数が入るだけのメモリを用意し、その中に順次値を放り込んでいけばいいです。そしてそのメモリの先頭ポインタをva_listとします。
先程の例と同じ引数列を再現するなら、以下のようになります。

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

int main()
{
    // 値を格納するエリアを確保する
    va_list val_area = new char[sizeof(int) + sizeof(double) + sizeof(char*)];
    // 値を書き込むためエリア内を移動するポインタ
    char *a_ptr = val_area;

    // エリアに値を順次設定
    int ival1 = 100;
    memcpy( a_ptr, &ival1, sizeof(int) );
    a_ptr += sizeof(int);

    double fval = 3.14;
    memcpy( a_ptr, &fval, sizeof(double) );
    a_ptr += sizeof(double);

    char *str = "piyo";
    memcpy( a_ptr, &str, sizeof(char*) );
    a_ptr += sizeof(char*);

    // 表示
    vprintf( "%d %f %s\n", val_area );
    delete[] val_area;
}

va_argマクロを使って簡潔に書く事もできます。

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

int main()
{
    // 値を格納するエリアを確保する
    va_list val_area = new char[sizeof(int) + sizeof(double) + sizeof(char*)];
    // 値を書き込むためエリア内を移動するポインタ
    char *a_ptr = val_area;

    // エリアに値を順次設定
    va_arg( a_ptr, int ) = 100;

    va_arg( a_ptr, double ) = 3.14;

    va_arg( a_ptr, char* ) = "piyo";

    // 表示
    vprintf( "%d %f %s\n", val_area );
    delete[] val_area;
}

補足

va_listが指し示すメモリは4バイトの倍数で区切ります。char型でもshort int型でも4バイトの領域(つまりint型)に入れます。
printfはfloat型は扱えないので、float型を入れてもうまく動作しません。