yohhoyの日記

技術的メモをしていきたい日記

可変引数リストと文字列とヌルポインタの特別な関係

C言語の可変引数リストと文字列(文字型へのポインタ)とヌルポインタの関係についてメモ。

下記コードではvoid*型をもつマクロNULLの値*1を、関数f実装ではchar*型として展開するが、両ポインタ型は互換型(compatible type)とみなされない*2。関数呼び出し側における実引数の型と、マクロva_arg呼び出しで指定する型は、互換型でないと未定義動作(undefined behavior)を引き起こす。ただし、特例によりvoid*char*(およびその互換型)同士はマクロva_argによる変換が許容されている。

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

void f(char *s, ...)
{
  va_list ap;
  va_start(ap, s);
  do {
    printf("%s\n", s);
    s = va_arg(ap, char*);
    // 3回目のva_arg呼び出しはヌルポインタ(NULL)へ展開されるが、
    // void*型をchar*型として展開するのは特例によりwell-defined
  } while (s);
  va_end(ap);
}

f("abc", "123", "xyz", NULL);  // OK

つまりC99標準規格に厳格に従う場合、可変引数リストの関数でヌルポインタを終端マーカとして利用するケースで、char*以外とNULLとの相互変換は未定義動作を引き起こす。

struct X { /*...*/ };
void h(X* p, ...) {
  // va_arg(ap, X*)を用いた実装
}
X x1, x2;

h(&x1, &x2, NULL);  // NG: void*とX*は非互換型のため未定義動作!
h(&x1, &x2, (X*)NULL);  // OK: X*型へのキャスト明示が必要

C99 7.15.1.1/p2より引用(下線部は強調)。

2 The va_arg macro expands to an expression that has the specified type and the value of the next argument in the call. The parameter ap shall have been initialized by the va_start or va_copy macro (without an intervening invocation of the va_end macro for the same ap). Each invocation of the va_arg macro modifies ap so that the values of successive arguments are returned in turn. 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.

おまけ:マクロNULLの代わりに、C++11で導入されたヌルポインタリテラルnullptrを用いても本記事の内容は成り立つ。nullptrの型はstd::nullptr_tであり*3、同型を可変引数リストの実引数に指定するとvoid*へと変換される*4
2022-09-06:次期標準C2x(C23)ではC++同様に専用のヌルポインタ定数nullptrが導入され、可変引数リストを経由してもvoid*およびchar*互換型へと安全に変換できる。id:yohhoy:20220906 参照。

関連URL

*1:厳密にはマクロ NULL は処理系定義(implementation defined)のヌルポインタ定数へ展開される(C99 7.17/p3)。C標準規格の範囲内だけで解釈した場合、マクロ NULL は事実上 (void*)0 へと展開される。id:yohhoy:20120503 も参照のこと。

*2:C99 6.7.5.1/p2: "For two pointer types to be compatible, both shall be identically qualified and both shall be pointers to compatible types."

*3:C++11 2.14.7/p1: "It is a prvalue of type std::nullptr_t."

*4:C++11 5.2.2/p7: "An argument that has (possibly cv-qualified) type std::nullptr_t is converted to type void*."