[C] 意外と知られていないva_copy

 C言語で可変長引数を扱うときに使うマクロと言えばva_start, va_end, va_argですが、実はもうひとつva_copyっていうマクロがあるんです。めったに使う機会のないこのマクロですが、ごく限られた状況で使う必要が出てきます。今回はそれをメモっておこうかなと。

可変長引数とは

 いまさらですが、可変長引数が何なのかをざっとおさらいしておきましょう。

 可変長引数は、printfとかで使われている、任意の数の引数を取ることができる引数のことです。printfでは最初にフォーマットを渡したあと、そのフォーマットに埋め込む数値なり何なりを並べて渡すわけですが、この部分が可変長引数です。printf系やscanf系の関数以外で使うことはまずないとは思いますが…

va_copyとは

 va_copyは、その名の通りva_list型の変数というか変数の状態をコピーするマクロです。C99で追加されました。なんでこんなものがあるかというと、va_argを呼び出す度にva_listの指し示す引数の位置が変わってしまうからです。つまり、一度va_argを呼び出してしまうと、もとの状態に戻すことができないのです。

va_list ap;
va_start(ap, argc);
int sum = 0;

for(int i = 0; i < argc; i++)
    sum += va_arg(ap, int);

va_end(ap);

 上記コードは引数がargc(可変長引数の個数)と可変長引数で、渡された数の総和を出す関数、と思ってください。例えば上記みたいなコードがあるとすると、va_argは呼び出される度にapの状態を変更します。したがって、以下の様なコードは意図したとおりに動きません(Windowsで試したら動くんだなーこれが…。Macだと動きませんでした。)

va_list ap;
va_start(ap, argc);
int sum = 0, prod = 1;

for(int i = 0; i < argc; i++)
    sum += va_arg(ap, int);

for(int i = 0; i < argc; i++)
    prod *= va_arg(ap, int);

va_end(ap);

 総乗を求めるコードを追加しましたが、2つ目のループに入る直前ですでにapは最後の引数まで読み込みが終わっています。2つ目のループでva_argを呼び出していますが、ここでどんな値が読み込まれるかはわかりません。もちろん読み込んじゃダメなんですが。

 こういうコードを書くシチュエーションはあまりないのですが、vsnprintfを使う場合にこのパターンが出てきます。

 まずvsnprintfですが、以下の様な定義になっています。

int vsnprintf(
    char * restrict s,
    size_t n,
    const char * restrict format,
    va_list arg
);

 ヘッダファイルはstdio.hです。
「なんとかprintf」みたいな関数群の命名規則ですが、以下のようになっています。

v 可変長引数型(va_list)を引数の最後に取る
s 標準出力ではなく、文字列バッファに結果を返す
n 出力する文字数を指定する

 こんな感じです。WindowsのCRTな関数群だと「t」(マルチバイトとUnicodeの切り替えをマクロで行う)とか「_s」(セキュアな関数)とか「_l」(ロケール指定)とかが付きますがまぁそれがおいといて、上記の文字が先頭に上記の順で付きます。つまりvsnprintfは、sprintfに文字数上限の指定と、可変長引数ではなく可変長引数型(va_list)を引数として取る関数です。

 この手(なんとかprintf系)の関数のもう一つの特徴として、出力された文字列の文字数を結果として返す、という点があります。vsnprintfでは、第二引数(出力する文字数上限)を0にすると、実際に出力は行われず、出力する文字数だけが結果として返ってきます。この時、第一引数(出力先バッファ)にはNULLを指定しても問題ありません。

 つまり、この関数を最初に呼び、文字数を取得した上でその文字数分のメモリを確保してやれば、出力すべき文字数分ぴったりのメモリを確保することが出来るわけです。

va_list ap;
va_start(ap, format);

const int len = vsnprintf(NULL, 0, format, ap);
char *buf = (char *)malloc((len + 1) * sizeof(char)); // NULL文字の分
vsnprintf(buf, len, format, ap);

va_end(ap);

 こんな感じですね。

 さて、ここで先ほど問題になった通り、apを二度使うことができません。そこで、va_copyの出番です。

va_list ap, ap2;
va_start(ap, format);
va_copy(ap2, ap); // ap → ap2へ状態をコピー

const int len = vsnprintf(NULL, 0, format, ap);
char *buf = (char *)malloc((len + 1) * sizeof(char)); // NULL文字の分
vsnprintf(buf, len, format, ap2); // ap2を引数に渡す

va_end(ap);
va_end(ap2);

 これでおっけーです。変数bufには展開された文字列が代入されています。使い終わったらメモリをちゃんと解放しましょう。1つ目の総和と総乗を計算する例だと、以下のようになりますね。

va_list ap, ap2;
va_start(ap, argc);
va_copy(ap2, ap);
int sum = 0, prod = 1;

for(int i = 0; i < argc; i++)
    sum += va_arg(ap, int);

for(int i = 0; i < argc; i++)
    prod *= va_arg(ap2, int);

va_end(ap);
va_end(ap2);

 ちなみに上記の例なら、va_copyを使わずともva_startをもう一度呼び出せば同じような挙動になります。

Visual C++な人は

 もっと先に書くべきだったかもしれませんが、Visual C++ではva_copyが定義されていません。そもそもva_copyはC99規格であって、C99の全てがC++に実装されているわけではありません。したがって、va_copyを自分で定義しないとダメなようです。

#ifndef va_copy
#    define va_copy(dest, src) ((dest) = (src))
#endif

 こんな感じです。この書き方がすべての場合で正しいわけではありません。可変長引数は、スタック上に引数が連続して並ぶことを利用して引数を可変個指定するので、大抵の場合はこれでうまくいく、というだけの話です。この場合の動作は未定義ですが、ほとんどのアーキテクチャ上で問題なく動作するようです。

やってはいけないこと

 va_copyでやってはいけないこととして、va_listの再初期化のためにこのマクロを使用することが挙げられます。一度使用されたva_listは、必ずva_endを呼んでから再度va_copyを使うようにしてください。もちろんva_copyだけでなくva_startにも言えることですが…

 以上、ちょっとマニアックで使い所が微妙なva_copyの紹介でした。

0 件のコメント :

コメントを投稿