C言語の可変引数リストアクセス用 va_arg マクロにおける奇妙な制限事項についてメモ。

va_argマクロの第二引数へ指定する型名には、“関数ポインタ型” や “配列へのポインタ型” を直接記述できない。ただしtypedefによる別名であればOK。こんなコード書くやつおらんやろ

#include <stdarg.h>

void f0(int x, ...)
  va_list ap;
  va_start(ap, x);
  // NG: 関数ポインタ型 int(*)(int) は直接指定できない
  int (*pf)(int) = va_arg(ap, int(*)(int));
  // NG: 配列へのポインタ型 int(*)[N] は直接指定できない
  int (*pa)[3] = va_arg(ap, int(*)[3]);

void f1(int x, ...)
  typedef int (*PF)(int), (*PA)[3];
  va_list ap;
  va_start(ap, x);
  // OK: typdefされた別名 PF ならば指定可能
  PF pf = va_arg(ap, PF);
  // OK: typdefされた別名 PA ならば指定可能
  PA pa = va_arg(ap, PA);

int h(int n);
int a[3];
f0(0, &h, &a);
f1(0, &h, &a);

C99 p1-2より一部引用(下線部は強調)。

#include <stdarg.h>
type va_arg(va_list ap, type);

The va_arg macro expands to an expression that has the specified type and the value of the next argument in the call. (snip) The parameter type shall be a type name specified such that the type of a pointer to an object that has the specified type can be obtained simply by postfixing a * to type. If there is no actual next argument, or if type is not compatible with the type of the actual next argument (as promoted according to the default argument promotions), the behavior is undefined, except for the following cases:

  • one type is a signed integer type, the other type is the corresponding unsigned integer type, and the value is representable in both types;
  • one type is pointer to void and the other is a pointer to a character type.

一見すると奇妙なこの制限は、va_argマクロの実装を考慮して設けられた。プリプロセッサでは単純なトークン列操作しか行えないため、型名に*を後置するだけで妥当なポインタ型名を生成できる必要がある。(PDF)C99 Rationaleより引用(下線部は強調)。 The va_arg macro
Changing an arbitrary type name into a type name which is a pointer to that type could require sophisticated rewriting. To allow the implementation of va_arg as a macro, va_arg need only correctly handle those type names that can be transformed into the appropriate pointer type by appending a *, which handles most simple cases. Typedefs can be defined to reduce more complicated types to a tractable form. When using these macros, it is important to remember that the type of an argument in a variable argument list will never be an integer type smaller than int, nor will it ever be float (see §

va_arg can only be used to access the value of an argument, not to obtain its address.

例:古の GCC 2.95.3*1 では下記マクロ定義となっていた(読みやすさのため簡略化している)

#define va_arg(AP, TYPE)  \
  (AP = (va_list) ((char *) (AP) + __va_rounded_size(TYPE)),  \
   *((TYPE *) ((char *) (AP) - __va_rounded_size(TYPE))))

/* __va_rounded_size(T) := sizeof(T)をsizeof(int)の倍数に切り上げ */

GCC 3.0以降*2LLVM/Clang*3では組み込み関数__builtin_va_argにより実装されるため、厳密に同制限を守らないコードでもコンパイルできてしまう。
VisualC++ 2017(MSVC 19.10)の場合、同制限に違反するコードは(一見すると不可解な)コンパイルエラーを引き起こすが、このMSVCの振る舞いはC言語仕様準拠といえる。