Super Technique 講座

可変長引数マクロ

可変長引数関数とは、皆さんお馴染みの printf 属、scanf 属関数の引数である、アレである。フツーの関数は引数の数が固定されており、いくつでも引数を勝手に渡せる、という仕様は例外である。あと、execl(3) やＸツールキットで使われる XtVaCreateManagedWidget() などの場合も、やはり引数がいくつでも渡せるが、この場合には最後の引数が NULL じゃないといけない、という制限があったりする。こういう妙チクリンな関数はどうやって実装しているのだろう？？？

stdarg.h とか varargs.h について

変な見出しである。実はかつてはこういうヘッダファイルがインクルードディレクトリに存在し、その中にこういう可変長引数を扱うためのマクロが入っていたのである。しかし、どういうわけか最近の標準的なヘッダファイルのセットの中には入っていない。これらは別にユーザプログラムとしてマクロを書いても良いものであり、これが可変長引数を実現する上で大変役立つのである。

たとえば printf(3) をラップする関数を書いてみたいとする。つまり、printf(3) の可変長引数をそのままに引数として受けて、printf(3) に引数をそのまま渡して、アボートする関数、名付けて fatal() を実装してみよう。これは次のようになる。

#include <stdio.h> #include <stdarg.h> void fatal( char *fmt, ... ) { va_list argp; va_start( argp, fmt ); vfprintf( stdout, fmt, argp ); exit( 1 ); }

呼び出しは普通の printf(3) に準じて出来る。たとえば次の通り。

fatal( "fatal: %s %d %g

", "test", 10, 3.1415 );

しかし、実は現在の Linux では大変面妖なことに、/usr/include を見ても stdarg.h などというファイルはないのである。実はこの stdarg.h というファイルは、

/usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/include/stdarg.h

などという妙なところ（要するに gcc が内部的に利用するライブラリ）にあるのである。これは要するに、言語仕様（「, ...」 などという引数定義）とも関わりがあり、しかもターゲットマシンによって微妙に実装が違うという厄介なものであるからであろう。（ちなみに stdarg.h は ANSI-C 版であり、varargs.h はそれよりも古い UNIX で使われていたもの。）

さて、もう少し丁寧に先程のソースを見ていこう。

まず fatal() の引数定義である。「 char *fmt, ... 」 などという妙な定義がされている。第１引数の char *fmt はともかくとして、「 ... 」が、これこそが可変長引数（というか、正確には「引数のチェックをしない」こと）の宣言なのである。じゃあ、完全に引数が不定である関数が定義できるか、というとそうではない。少なくとも１つの固定引数がなくてはならないのである。言い替えれば、最低１つ引数があることが保証されれば、それ以降は「 ... 」で宣言をして不定個の引数にできるのである。これは va_start マクロを使うために絶対に必要なことなのである。

次の「 va_list argp; 」という宣言は大したものではない。どうせ va_list は void * か char * あたりを typedef したものに過ぎない。

次の「 va_start( argp, fmt ); 」は問題だ。これは第１引数（正確には不定引数になる前の最後の固定引数）と、va_list で宣言されたローカル変数を取って、最初の不定引数のポインタを計算してマクロ第１引数に代入するマクロである。これは後でちゃんと見るが、今は単に結果として argp に fmt の次の引数のポインタが入っていると考えれば良い。

そして、vfprintf(3) を呼び出す。これは fprintf(3) の変形で、通常の第１引数の出力FILE構造体、第２引数のフォーマットの次に第３引数としてこの va_list 型を取るという、固定引数タイプの fprintf(3) なのである。だから、結果として va_startマクロによって受けた不定長引数が va_list 型に変換されて、vfprintf(3) に渡される、ということになる。

引数って何？

関数には引数が付き物だ（引数がないものがあるのは勿論）。この引数はよく考えると実は多様なのである。なぜならコンパイラは常に最適化と向き合っている。だから、出来るだけ効率の良いやり方で引数を渡したい、とコンパイラは考える。また、他の言語で書かれたサブルーチンを呼び出すこともたまにはある。その時に引数の渡し方が違っていたりしたらとんでもない結果を返してくれることになる。だから、ある言語では「デフォルトの引数の渡し方」というものがちゃんと決まっており、それに従ってコンパイラは関数を呼ぶ準備の中で引数を処理し、関数の中で引数にアクセスできるのである。

Ｃ言語では、printf(3) 属などの不定長引数関数をどうしても実現したかったために、やや効率の悪い手法を「デフォルトの引数の渡し方」に採用した。呼び側がスタックに引数を積んで、最後にリターンアドレスを引数に積んで、関数が示すアドレスにジャンプするのである。だから、関数内部では呼ばれた時のスタックポインタからの相対的な位置によって引数にアクセスできる。関数が戻った後には、呼び側の責任として積んだ引数スタックを破棄する。疑似アセンブリで示せば次のような処理になる。

/* sub( 1, 2, 3 ); */ MOV EAX, 3 PUSH EAX /* 引数の最後からスタックに積む */ MOV EAX, 2 PUSH EAX MOV EAX, 1 PUSH EAX CALL sub /* すべて引数をスタックに積んだら、CALL する。*/ ADD ESP, 12 /* 戻ってきたら、積んだスタック（３個×int(4byte)）を破棄する */

関数内部では次のように引数にアクセスする。

/* sub( int a1, int a2, int a3 ) { */ /* return a1 + a2 + a3; */ PUSH EBP /* いわゆるベースポインタ（関数突入時のスタックポインタのコピー） */ MOV EBP, ESP MOV EAX, [EBP + 4] /* 第１引数をAXレジスタに */ ADD EAX, [EBP + 8] /* 第２引数をAXレジスタに加算 */ ADD EAX, [EBP + 12] /* 第３引数をAXレジスタに加算 */ POP EBP /* EBP を元に戻す */ RET

だから、引数の個数については関数の呼び側が責任を持っているのである。それゆえ、不定個の引数であっても、実際に積んだ引数の数は、積んだ側なので知っており、それを破棄することができる。これが不定長引数のトリックの根本にある。

もし、不定長引数がなければ、関数側で積んだ引数を破棄（スタックポインタを戻す）することも可能である。これは Pascal などの言語が採用しているやり方である。関数側に引数破棄が入るので、複数ありうる呼び側処理がわずかに減ることになる。しかし、これだと不定引数関数は実現できない。

また、このスタック経由のやり方の鍵の一つは、なるべく引数のサイズが均一であることでもある。今時の 32bit ＣＰＵでは、int 型は比較的大きなデータオブジェクトである。これよりも大きいデータオブジェクトは、浮動小数点数を示すもの（float, double）と gcc の独自拡張である「long long 型」（知ってた？こんなの）か、あまりしない構造体（ポインタではなく）引数渡しくらいなものである。だから、引数はポインタであろうと char 型であろうと、とにかく 4byte データ（レジスタのサイズ）としてスタックに積まれる。これによって問題が大いに単純になっているのである。

ちなみにＣ言語には register 修飾子があり、これは関数引数に対しても使える。たとえば、

int sub( register int a1, register int a2 );

の要領である。この修飾をすると、もし可能であれば（引数の個数と使えるレジスタの数との兼ね合いがある）関数呼び出しの時にスタック経由ではなくて、レジスタ経由で関数に引数が渡ることになる。疑似アセンブリで示せば次の通り。

/* sub( 1, 2 ); */ MOV ESI, 2 /* ESI, EDI レジスタを使って２つの引数を渡す */ MOV EDI, 1 CALL sub /* すべて引数をスタックに積んだら、CALL する。*/

まあ、この機能は今時の賢い gcc では、インライン関数展開による最適化のカラミで、相対的に比重が落ちている。どうせインライン関数展開をさせてしまえば、当然レジスタ経由で、引数が渡るのである。まあ、今時 register 修飾をして、最適化を稼ぐのは時代遅れである（馬鹿なコンパイラを使っている人は知らんが...）。

さて、話を不定長引数処理に戻そう。呼び側では引数が不定個であっても、それを積んだ本人なのでいくつ積んだかは判っており、スタックを戻すのには支障はない。また、スタックに引数を積む時に、デフォルトでは（というのか、不定長引数を積む場合には必ず）引数並びの最後の物からスタックに積んでいくという規則がある。それゆえ、関数に突入した際には、必ず「[ESP+4]」で第１引数にアクセス出来るのである（勿論引数がない場合は除く）。

それならば、次のようなやり方で不定長引数にアクセスできることになる。（ただし、細かいことを言えばスタックが上方伸長型で、int 単位で、標準的なスタック経由の関数呼び出しをする、という前提の下でだが...）

/* int型不定個の引数の総和を返す */ int adds( int n, ... ) /* n は引数の個数 */ { int ret = 0; int *v = &n; while( n-- > 0 ) { ret += *++v; } return ret; }

実際には printf(3) がしていることも、これと大差がない。引数の個数をどうやって判定しているのか、といえば、それは書式の中にある「%」の数を見、「%f」などの浮動小数点の値などは適切に複数個のスタックデータから値を構築して、スタックに積まれたデータを処理しているのである。それゆえ、ちょっと実験をしてみるのならば書式の「%」の数を実際に仮引数として与える引数の個数よりも増やして見ればよい。

void main() { int x = 666; printf( "%d %d %d

", 69, 13 ); }

筆者の実行環境では出力は「69 13 666」となった（結果が違う可能性はある）。面白いことに、ちゃんとスタックに確保された自動変数である x の値が余計な「%d」の値として出力されている。これはスタックの上に連続して自動変数と引数が確保されたことを示しているのである。

だから、不定長引数の場合には、次のやり方のどれかで関数側が実際にいくつの引数が積まれたのかを知ることができなければならない。

不定引数の個数を第１引数として渡す。adds() で使ったやり方。 第１引数が「書式」である。printf属、scanf属が使うやり方。 とにかく最後の引数が NULL であることにする。これは exec属や XtVaCreateManagedWidget() などが使っているやり方。

まあ、このような「スタックの使い方」が不定長引数関数の基盤となっているのである。実際には va_start マクロはこのようなスタックへのポインタをうまく汎用的に扱うマクロに過ぎないのである。

va_arg マクロの使用例

実際には stdarg.h では、va_start(), va_arg(), va_end() の３つのマクロを使って、可変長引数を順次処理していく。先程の vfprintf(3) を使った例では最初のポインタだけでＯＫで、後続の不定引数の処理は vfprintf(3) にまったく任せてしまったので、va_start() マクロしか使わなかったのである。そうではなくて、まったく自前で不定引数を処理するプログラムをサンプルとして書いてみる。

この時、やはり不定引数の型の問題（というか、その型のサイズが int よりも大きいケース）について配慮が必要になる。このようなスタックに積まれた引数サイズの不整合の問題を、実は va_arg() マクロはうまく解決してくれるのである。だから、va_arg() マクロを使う限り、int よりも大きなサイズの型についての問題を気にしなくてもよくなる。それだけではなくて、適切にキャストして正しく数値を合成できることをも保証してくれる。これは、スタックの２つ以上の int サイズの値を合成してより大きな型の値にする時に、ビッグエンディアン or リトルエンディアンの問題が生じるためである。このように「良く出来た」マクロなのである（ただし、GNU のものの話に限るけど...他は知らんよ）。

たとえば、(「型」「値」)* が繰り返し引数に現われるタイプの不定引数でサンプルを示そう。つまり、int 型の「次の引数の型」を表す定数マクロがあり、その次に型によって示された値がある、というタイプである（強いて言えば XtVaCreateManagedWidget() の不定引数の渡し方がこれだな）。まず、続く引数の「型」を表すマクロはこんなものだ。

#define END 0 /* これで不定引数は終り */ #define CHAR 1 #define SHORT 2 #define INT 3 #define LONG 4 #define FLOAT 5 #define DOUBLE 6 #define STRING 7 /* char * も出来る */ #define LONGLONG 8 /* gcc 専用の 64bit int */

だからこんな具合に呼び出す。

void main( ) { va_out( "", LONGLONG, 4294967296LL, DOUBLE, 4.3, STRING, "test", INT, 69, DOUBLE, M_PI, CHAR, 'a', END ); }

申し訳ないが、第１引数はダミーである。単に char * で渡すことにする。これは当然 va_start() マクロで argp を取得するのにどうしても必要だからである。さて、va_out() の定義は次の通り。

void va_out( char *dummy, ... ) { va_list argp; int kind; va_start( argp, dummy ); /* 最初の不定引数を取得 */ while( 1 ) { kind = *(int *)argp; /* 現実には va_list は void * である */ va_arg( argp, int ); /* int 型スタックをスキップして飛ばす */ switch( kind ) { case END: va_end( argp ); return; break; case CHAR: printf( "char %c

", *(char *)argp ); va_arg( argp, char ); break; case SHORT: printf( "short %d

", *(short *)argp ); va_arg( argp, short ); break; case INT: printf( "int %d

", *(int *)argp ); va_arg( argp, int ); break; case LONG: printf( "long %ld

", *(long *)argp ); va_arg( argp, long ); break; case FLOAT: printf( "float %g

", *(float *)argp ); va_arg( argp, float ); break; case DOUBLE: printf( "double %g

", *(double *)argp ); va_arg( argp, double ); break; case STRING: /* 文字列はダブルポインタ */ printf( "string %s

", *(char **)argp ); va_arg( argp, char * ); break; case LONGLONG: /* long long だって問題ない */ printf( "long long %Ld

", *(long long *)argp ); va_arg( argp, long long ); break; } } }

一応これでうまく行くのである（ホントは FLOAT だけちょっと怪しい...バグのようだ）。

FORTRAN 風書式出力

ということは、printf(3) のやっていることは基本的にこういうやり方に、書式解析を付け加えているに過ぎない。交互に「型」と「値」をスタックに積むのではなくて、「型」の情報が「書式」に埋め込まれていて、それを適切に型情報として読み込んで、引数を処理しているのである。しかし、printf(3) をそのまま模倣してもツマラないので、これをヒネって FORTRAN 風書式を処理するサブルーチンを書いてみよう。

FORTRAN の書式指定は結構複雑だが、そのサブセットということで許して欲しい。FORTRANの書式は大体次のようなルールで出来ている。

各項目は「,」で区切られる。 項目は「'〜'」のようなそのまま出力される「リテラル文字列」か、引数を解釈して変換して出力する「出力指定」である。 「出力指定」はその出力型に応じて、「空白出力」、「文字出力」、「数値出力」がある。 「空白出力」は「10X」のようなかたちで、１０文字の空白を出力する。 「文字出力」は「5A10」のようなかたちで、１０文字の文字列が入っている引数を５つ使って出力する。要するにFORTRANでは、WRITE命令の引数として、配列を一種のループと共に渡すことが出来るのである。ちゃんとFORTRANの仕様に合わすとＣでは実装が難しいので、このかたちで指定した時には正しく配列が５要素あることに制限する。もちろん「A50」のような指定もできる。 「数値出力」もやはり「文字出力」と同様に配列を指定できて「3I4」のように指定する。これは数値４桁の数の３要素の配列があることを指定する。勿論「I4」のように配列ではない数値も指定できる。

浮動小数点値と改行指定はサボっている。申し訳ない。このサブセットの書式の例は次の通り。

10 FORMAT(5X,'output is ',3A5,2X,I4,4X,I4) WRITE(6,10) (MESS(I),I=1,3),DATA1,DATA2

これがＣのプログラムだと次のようになるわけだ。

char *mes[] = { "test", "my", "form" }; int data1 = 10; int data2 = 222; fortan_write( "5X,\'output is \',3A5,2X,I4,4X,I4", mes, data1, data2 );

出力は当然両方とも次の通り。

12345678901234567890123456789012345678901234567890 output is test my form 10 222

まずライブラリ自身が使用するヘッダファイルである。struct Format は、一つ一つの書式指定を解析した結果であり、これらが next メンバに次の書式の解析結果を繋げた連結リストとして返る。

#define LITERAL 0 /* '〜' に対応する、リテラルの文字列 */ #define BLANK 1 /* X に対応する、空白出力 */ #define ASCII 2 /* A に対応し、引数から文字列を出力 */ #define NUMBER 3 /* I に対応し、引数から数値を出力 */ struct Format { int kind; /* LITERAL などの種別 */ union u { char *mess; /* LITERAL の場合、文字列ポインタを保持 */ struct form { /* BLANK, ASCII, NUMBER の場合 */ short repeat; /* 繰り返し数(前の数値) */ short num; /* 個別の書式の表示長さ(後の数値) */ } form; } u ; struct Format *next; /* 次の書式指定 */ }; /* 新しい Format のオブジェクトを取得。本質的に malloc(3) をラップ */ static struct Format *newFormat( void ); /* 制作された struct Format を解放。いわゆる cdr ダウンをして再帰的に解放する。 */ static void deleteFormat( struct Format *f ); /* 書式文字列から struct Format 連結リストを取得する */ static struct Format *parse_fmt( char *fmt );

外部インターフェイスは当然これ。

int fortran_write( char *fmt, ... );

ではライブラリコード。まず fortran_write() 自身を示す。

int fortran_write( char *fmt, ... ) { struct Format *ret, *fm; char work[256]; va_list argp; va_start( argp, fmt ); /* 不定引数処理の準備 */ ret = parse_fmt( fmt ); /* 書式を解析して Fomat 構造体に変換 */ if( ret == NULL ) { fprintf( stderr, "parse failed! %s

", fmt ); return -1; } fm = ret; while( fm ) { /* 一つ一つの書式を処理 */ switch( fm->kind ) { case LITERAL: /* '〜' で示されるリテラル文字列 */ write( 1, fm->u.mess, strlen(fm->u.mess) ); break; case BLANK: /* X で示される空白出力 u.form.num メンバは無視 */ memset( work, ' ', sizeof(work) ); write( 1, work, fm->u.form.repeat ); break; case ASCII: /* A で示される引数を使った文字列出力 */ memset( work, ' ', sizeof(work) ); if( fm->u.form.repeat > 1 ) { /* 配列を受ける場合 */ int i, total; char **array = *(char ***)argp; /* 本質的なキャスト */ /* 出力の制作（左寄せ、文字数超過はカット） */ for( i = 0; i < fm->u.form.repeat; i++ ) { my_strncpy( &work[i * fm->u.form.num], array[i], fm->u.form.num ); } /* 結果の総文字数を得る */ total = fm->u.form.num * fm->u.form.repeat; va_arg( argp, char **); /* argp を進める */ write( 1, work, total ); } else { /* 単一文字列を受ける場合 */ char *mes = *(char **)argp; /* 今までと大差なし */ my_strncpy( work, mes, fm->u.form.num ); va_arg( argp, char *); /* argp を進める */ write( 1, work, fm->u.form.num ); } break; case NUMBER: /* I で示される、引数を使った数値出力 */ memset( work, ' ', sizeof(work) ); if( fm->u.form.repeat > 1 ) { /* 配列を受ける場合 */ int i, total; int *array = *(int **)argp; /* 本質的なキャスト */ /* 出力の制作（右寄せ、文字数超過はカット） */ for( i = 0; i < fm->u.form.repeat; i++ ) { setnum( &work[i * fm->u.form.num], array[i], fm->u.form.num ); } /* 総出力文字数の計算 */ total = fm->u.form.num * fm->u.form.repeat; va_arg( argp, int *); /* argp を進める */ write( 1, work, total ); } else { /* 単一の数値を受ける */ int mes = *(int *)argp; setnum( work, mes, fm->u.form.num ); va_arg( argp, int ); /* argp を進める */ write( 1, work, fm->u.form.num ); } break; default: fprintf( stderr, "cannot happen

" ); deleteFormat( ret ); /* Format 構造体の破棄 */ return -1; break; } fm = fm->next; /* 次の Format 構造体 */ } va_end( argp ); /* argp の破棄 */ deleteFormat( ret ); /* Format 構造体の破棄 */ return 0; }

面白いのは赤字で強調した部分、

char **array = *(char ***)argp; /* 本質的なキャスト */

である。キャストの見かけ上だが、トリプルポインタになっている。一般論としてトリプルポインタは、複雑な多次元データ構造を扱う場合に出ないわけでもないのだが、まあ、あまり乱用すべきものではない。絶対に混乱するので、グローバル変数として定義して、多次元配列調にアクセスするのが普通であろう。筆者のプログラミング経験でも、本質的にトリプルポインタ以上を使ったことがあるのは、この不定長引数処理だけである。これが当然、fortran_read() などという入力用関数を定義するとなると、上のキャストは次のようになる。

char ***array = *(char ****)argp;

こうなると、さすがの筆者もちょっと恐怖である（辞書によると、クォドゥルプル(quadruple)・ポインタか..）。元気の良いプログラマは一度チャレンジして見たまえ。

この中でちょっとした下請け関数を使っている。出力整形用に過ぎない。

/* 数値を文字列に変換する */ /* バッファ buf に１０進数で val の値を num 文字分だけセット。超過はカット、不足分は空白 */ /* 手抜き...なぜなら書式長さを越えて「-」が必要な時にどうしたら良いかわからない。 よって負の数には対応せず。*/ static void setnum( char *buf, int val, int num ) { int i; for( i = num - 1; i >= 0 && val > 0; i-- ) { buf[i] = (val % 10) + '0'; val /= 10; } } /* 文字列を len 文字分コピー。strncpy(3) みたいなもの。 しかし、文字列末尾に \0 を追加しない。*/ static void my_strncpy( char *d, char *s, int len ) { int i; for( i = 0; i < len && *s; i++ ) { *d++ = *s++; } }

問題は書式から struct Format に変換するルーチンである parse_fmt() である。この程度ならば強引に書いても書けるし、正規表現解析ルーチンを使えば少しは楽だ。しかし、拡張性とかデバッグ適性を考えると、この内容はちょっと難しいので、実際には「有限状態機械」のロジックを使った方が無難である。そのため、このルーチンの解説は「可変長引数マクロ」の趣旨からは外れるので、「有限状態機械」を参照されたい。

copyright by K.Sugiura, 1996-2006