Subsections


15. Variable-Length Argument Lists

널리 알려지지는 않았지만, C 언어는 함수가 가변 인자를 (즉 인자의 갯수가 정해지지 않은) 받을 수 있는 기능을 제공합니다. Variable-length argument list(가변 인자 리스트)는, 드물기는 하지만, printf와 같은 함수들에게는 꼭 필요한 것입니다. (variable-length argument list는 ANSI C 표준에서 공식적으로 지원하지만, ANSI C 표준 이전에는 엄격히 말해서 정의되어 있지 않습니다.)

Variable-length argument list를 처리하는 방법은 상당히 기묘하기까지 합니다. 정식으로 varaible-length argument list는 fixed part(고정된 부분)와 variable-length part(가변 길이)의 두 부분으로 나누어져 있습니다. 우리는 “variable-length argument list의 variable-length part”라는 과장된 용어를 쓰고 있다는 사실을 발견했지만, 어쩔 수 없습니다 (혹시 여러분이 “variadic”이나 “varargs”라는 용어가 쓰이는 것을 보신 적이 있을지도 모르겠습니다: 두 가지 용어 모두 “having a variable number of arguments.15.1”을 뜻합니다. 따라서 “vararg function” 또는 “varargs argument”라고 말할 수도 있습니다.)

Variable-length argument list를 쓰는 방법은 크게 세 단계로 이루어 집니다. 먼저 va_list 타입의 특별한 pointer type을 써서 선언하고, 그 다음 va_start를 불러서 초기화합니다.

15.1 Calling Varargs Functions



Q 15.1
printf()를 부르기 전에 #include <stdio.h>를 쓰라고 하더군요. 꼭 그럴 필요가 있을까요?
Answer
적절한 프로토타입(prototype)을 현재 영역(scope) 안에 포함시키기 위해서 입니다.

어떤 컴파일러에서는 가변 인자 리스트를 쓰는 함수에는 일반 방식과는 다른 호출 순서(calling sequence)를 사용합니다. (It might do so if calls using variable-length argument lists were less efficient than those using fixed-length.) 그러므로 함수의 프로토타입이 (즉 “...”를 포함한 선언) 영역 안에 있어야 컴파일러가 가변 인자 리스트 처리 메커니즘을 사용할 수 있습니다.

References
[ANSI] § 3.3.2.2, § 4.1.6
[C89] § 6.3.2.2, § 7.1.7
[ANSI Rationale] § 3.3.2.2, § 4.1.6
[H&S] § 9.2.4 pp. 268-9, § 9.6 pp. 275-6



Q 15.2
printf()에서 %ffloatdouble 인자 모두에 쓰일 수 있는 이유는 무엇인가요?
Answer
가변 인자 리스트에서 가변 인자 부분에는 “default argument promotion” 이 적용됩니다: 즉, charshort int 타입은 int로 변경되며(promotion), float 타입은 double 타입으로 변경됩니다. (이 것은 함수의 프로토타입이 없거나 구 방식(old style)으로 선언된 함수에서 일어나는 `promotion'과 같은 것입니다; 질문 [*]11.3을 참고하기 바랍니다.) 그러므로 printf%f 포맷은 항상 double 타입만 받아들이는 셈이 됩니다. (비슷한 이유에서 %c, %hd 포맷은 항상 int만을 받아 들이게 됩니다.) 덧붙여 질문 [*]12.9, [*]12.13도 참고하시기 바랍니다.
References
[ANSI] § 3.3.2.2
[C89] § 6.3.2.2
[H&S] § 6.3.5 p. 177, § 9.4 pp. 272-3



Q 15.3
nlong int일 경우, 다음 코드에서 문제가 발생합니다:
  printf("%d", n);
그렇지만 ANSI 함수 프로토타입을 제공했으니, 자동으로 형 변환이 적용될 거라고 생각합니다. 무엇이 잘못되었나요?
Answer
함수가 가변인자를 받는다면, 프로토타입이 있더라도, 가변 인자에 대해서는 어떠한 정보도 알지 못하며, 또한 알 수도 없습니다. 따라서, 일반적으로, 가변 인자 리스트에서 가변 인자에 해당하는 부분은 그러한 보호를 (type conversion) 받지 못합니다. 컴파일러는 가변인자에 대해서는 implicit type conversion을 수행하지 않으며, type이 서로 맞지 않는 경우 경고 해 줄 수 있습니다. 따라서 프로그래머는 가변 인자로 들어간 인자가 예상하는 타입과 같은지 확인해야 하며, 다를 경우에는 반드시 캐스팅을 (explicit cast) 해 주어야 합니다:

어떤 컴파일러들과 (gcc 포함), 어떤 lint 프로그램은, format string이 문자열 상수(string literal)인 경우에 한해서, printf와 같은 함수들에 전달된 가변 인자가 정말로 올바른 타입으로 전달되었는지 검사해 주기도 합니다.

덧붙여 질문 [*]5.2, [*]11.3, [*]12.9, [*]15.2도 참고하시기 바랍니다.

Note
gcc의 경우, function attribute란 것으로 printf 또는 scanf와 같은 함수의 가변 인자 부분을 검사해 줄 수 있습니다. 자세한 것은 gcc 매뉴얼을 참고 바랍니다.

15.2 Implementing Varargs Functions



Q 15.4
가변 인자를 받는 함수를 어떻게 만들 수 있을까요?
Answer
<stdarg.h> 헤더 파일에 있는 기능을 사용합니다.

아래의 함수는 주어진 여러 개의 문자열을 붙여서 malloc()으로 할당한 메모리에 저장해서 리턴하는 함수입니다:

  #include <stdlib.h>     /* for malloc, NULL, size_t */
  #include <stdarg.h>     /* for va_ stuff */
  #include <string.h>     /* for strcat et al.  */

  char *vstrcat(char *first, ...)
  {
    size_t len;
    char *retbuf;
    va_list argp;
    char *p;

    if (first == NULL)
      return NULL;

    len = strlen(first);

    va_start(argp, first);

    while ((p = va_arg(argp, char *)) != NULL)
      len += strlen(p);

    va_end(argp);

    retbuf = malloc(len + 1);   /* +1 for trailing \0 */

    if (retbuf == NULL)
      return NULL;            /* error */

    (void)strcpy(retbuf, first);

    va_start(argp, first);      /* restart; 2nd scan */

    while ((p = va_arg(argp, char *)) != NULL)
      (void)strcat(retbuf, p);

    va_end(argp);

    return retbuf;
  }
(인자 리스트를 처음부터 다시 검색할 때에는 위에서처럼 va_start를 다시 불러 주어야 합니다. 또한 va_end가 실제로 하는 일이 없을 수는 있지만, 이식성을 위해서 반드시 불러주어야 합니다.)

사용법은 다음과 같습니다:

  char *str = vstrcat("Hello, ", "world!", (char *)NULL);
마지막 인자를 캐스팅한 것을 꼭 주의깊게 보시기 바랍니다; 질문 [*]5.2, [*]15.3을 참고하기 바랍니다. (또한 이 함수를 부른 함수는 이 함수가 리턴한 문자열을 free시켜 주어야 할 책임이 있습니다.)

위의 예는 모든 가변 인자의 타입이 char *입니다. 이제, 가변 인자의 갯수와 타입이 정해지지 않는 함수를 만들어 보겠습니다; 이 함수는 아주 간단한 기능만 지원하는 printf입니다. 특히 va_arg()에서 원하는 타입을 미리 지정한 부분에 대해 주의깊게 보시기 바랍니다.

(아래 miniprintf 함수는 질문 [*]20.10에서 쓴 baseconv를 씁니다. 또한 이 것은 INT_MIN과 같은, 가장 작은 정수 값을 올바르게 출력하기에 적당하지 않습니다.)

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

  extern char *baseconv(unsigned int, int);

  void
  miniprintf(char *fmt, ...)
  {
    char *p;
    int i;
    unsigned u;
    char *s;
    va_list argp;

    va_start(argp, fmt);

    for (p = fmt; *p != '\0'; p++) {
      if (*p != '%') {
        putchar(*p);
        continue;
      }

      switch (*++p) {
      case 'c':
        i = va_arg(argp, int);
        /* not va_arg(argp, char); 질문 15.10 참고 */
        putchar(i);
        break;

      case 'd':
        i = va_arg(argp, int);
        if (i < 0) {
          /* XXX won't handle INT_MIN */
          i = -i;
          putchar('-');
        }
        fputs(baseconv(i, 10), stdout);
        break;
 
      case 'o':
        u = va_arg(argp, unsigned int);
        fputs(baseconv(u, 8), stdout);
        break;

      case 's': 
        s = va_arg(argp, char *);
        fputs(s, stdout);
        break;

      case 'u':
        s = va_arg(argp, unsigned int);
        fputs(baseconv(u, 10), stdout);
        break;

      case 'x':
        u = va_arg(argp, unsigned int);
        fputs(baseconv(u, 16), sttout);
        break;

      case '%':
        putchar('%');
        break;
      }      
    va_end(argp);
    }
  }
덧붙여 질문 [*]15.7도 참고하시기 바랍니다.

References
[K&R2] § 7.3 p. 155, § B7 p. 254
[ANSI] § 4.8
[C89] § 7.8
[ANSI Rationale] § 4.8
[H&S] § 11.4 pp. 296-9
[CT&P] § A.3 pp. 139-141
[PCS] § 11 pp. 184-5, § 13 p. 242



Q 15.5
printf()와 같이 포맷 문자열을 받아들여 처리하는 함수를 만들어 그 처리를 printf()에게 맡기고 싶습니다.
Answer
vprintf(), vfprintf(), vsprintf() 함수를 쓰면 됩니다. 이 함수들은, 각각 printf(), fprintf(), sprintf()과 같은 일을 하며, 단지 마지막 인자가 가변 인자인 대신 va_list 타입에 대한 포인터를 받습니다.

예를 들어, 아래의 error() 함수는 에러 메시지를 받아들여 그 메시지 앞에 “error: ”를 덧붙이고 newline을 붙여서 출력해주는 함수입니다:

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

  void error(char *fmt, ...)
  {
    va_list argp;
    fprintf(stderr, "error: ");
    va_start(argp, fmt);
    vfprintf(stderr, fmt, argp);
    va_end(argp);
    fprintf(stderr, "\n");
  }
덧붙여 질문 [*]15.7도 참고하시기 바랍니다.

References
[K&R2] § 8.3 p. 174, § B1.2 p. 245
[C89] § 7.9.6.7, 7.9.6.8, 7.9.6.9
[H&S] § 15.12 pp. 379-80
[PCS] § 11 pp. 186-7



Q 15.6
scanf()와 같은 기능을 하는 함수를 만들고 싶습니다.
Answer
[C9X]는 vscanf()vfscanf(), vsscanf()를 지원할 것입니다. (물론 당장 하기 위해서는 여러분 스스로가 그러한 함수를 만들어야 합니다.)
References
[C9X] § 7.3.6.12-14
Note
C99 표준은 vscanf(), vfscanf(), vsscanf()를 지원합니다. 따라서 질문 [*]15.5와 같은 방법으로 만들면 됩니다.
References
[H&S2002] § 15.12 p. 401 [C99] § 7.19.1, § 7.19.6.9, § 7.19.6.11, § 7.19.6.14



Q 15.7
ANSI 이전의 컴파일러를 사용하고 있습니다. <stdarg.h>가 없는데 어떻게 하죠?
Answer
<stdarg.h>에 해당하는 오래된 헤더파일인 <varargs.h>를 쓰면 됩니다. 예를 들어, 질문 [*]15.4에서 만든 vstrcat을, <varargs.h>를 쓰도록 고치면 다음과 같습니다:
  #include <stdio.h>
  #include <varargs.h>
  #include <string.h>

  extern char *malloc();

  char *vstrcat(va_alist)
  va_dcl                       /* no semicolon */
  {
    int len = 0;
    char *retbuf;
    va_list argp;
    char *p;

    va_start(argp);

    while ((p = va_arg(argp, char *)) != NULL)
      len += strlen(p);

    va_end(argp);

    retbuf = malloc(len + 1);  /* +1 for trailing '\0' */
 
    if (retbuf == NULL)
      return NULL;             /* error */

    retbuf[0] = '\0';

    va_start(argp);            /* restart for second scan */

    while ((p = va_arg(argp, char *)) != NULL)
      strcat(retbuf, p);

    va_end(argp);

    return retbuf;
  }
(va_dcl뒤에 세미콜론(`;')이 없는 것에 주의하기 바랍니다. 그리고, 이 경우, 첫번째 인자에 대한 특별한 처리가 필요없습니다.) 또, <string.h>을 포함시키는 대신, 직접 문자열 처리 관련 함수들을 선언해 주어야 할 지도 모릅니다.

만약, 시스템이 vfprintf를 제공하며, <stdarg.h>를 제공하지 않을 경우, 아래에 (질문 [*]15.5에서 쓴) <varargs.h>를 쓴 error 함수를 보입니다:

  #include <stdio.h>
  #include <varargs.h>

  void error(va_alist)
  va_dcl                  /* no semicolon */
  {
    char *fmt;
    va_list argp;
    fprintf(stderr, "error: ");
    va_start(argp);
    fmt = va_arg(argp, char *);
    vfprintf(stderr, fmt, argp);
    va_end(argp);
    fprintf(stderr, "\n");
  }
(<stdarg.h>에서와는 달리, <varargs.h>에서는 모든 인자가 가변인자입니다. 따라서 fmt 인자도, va_arg를 써서 얻어야 합니다.)
References
[H&S] § 11.4 pp. 296-9
[CT&P] § A.2 pp. 134-139
[PCS] § 11 pp. 184-5, § 13 p. 250

15.3 Extracting Variable-Length Arguments



Q 15.8
함수에 몇 개의 인자가 전달되었는지를 정확히 알 수 있는 방법이 있나요?
Answer
호환성있는 방법은 존재하지 않습니다. 어떤 오래된 시스템에서는 비표준 함수인 nargs()를 제공하기도 합니다. 그러나 이 함수는 인자의 갯수를 리턴하는 게 아니라 전달된 word 갯수를 리턴합니다. (구조체나 long int, 실수는 여러 개의 word로 이루어져 있는 경우가 대부분입니다.)

가변 인자를 받아 처리하는 함수는 그 자체만으로 인자의 갯수를 파악할 수 있어야 합니다. printf 계열의 함수들은 포맷 문자열에서 (%d와 같은) 포맷 specifier를 보고 그 갯수를 파악합니다. (그렇기 때문에 printf()에 전달된 인자의 갯수가 포맷 문자열과 맞지 않을 경우에 오류를 일으킵니다.) 또 다른 방법으로, 가변 인자가 모두 같은 타입일 경우, 마지막 인자를 (0, -1, 또는 적절한 널 포인터와 같은) 어떤 특정한 값으로 설정해서 인자의 갯수를 파악합니다 (질문 [*]5.2, [*]15.4에서 execl()vstrcat() 함수의 사용법을 참고하시기 바랍니다). 마지막으로, 인자의 갯수를 미리 파악할 수 있다면, 전체 인자의 갯수를 인자로 전달하는 것도 좋은 방법입니다. (although it's usually a nuisance for the caller to supply).

References
[PCS] § 11 pp. 167-8



Q 15.9
제 컴파일러는 다음과 같은 함수를 정의하면 에러를 냅니다.
  int f(...)
  {
  }
Answer
표준 C에서는 va_start()를 쓰려면 적어도 하나의 고정된 인자가 있어야 한다고 말하고 있습니다. (어쨌든, 가변 인자의 갯수 또는 타입을 알려면, 하나 이상의 인자가 필요합니다.) 덧붙여 질문 [*]15.10도 참고하시기 바랍니다.
References
[ANSI] § 3.5.4, § 3.5.4.3, § 4.8.1.1
[C89] § 6.5.4, § 6.5.4.3, § 7.8.1.1
[H&S] § 9.2 p. 263



Q 15.10
가변 인자를 처리하는 함수에서 float 인자를 처리하지 못합니다. 왜 va_arg(argp, float)을 쓰면 동작하지 않을까요?

Answer
가변 인자 리스트에서 가변 인자 부분은 항상 “default argument promotion”이 적용됩니다: 즉 float 타입의 인자들은 항상 double로 변환되며, charshort int의 경우 항상 int로 변환됩니다.

따라서, va_arg(argp, float)은 잘못된 코드이며, 대신 va_arg(argp, double)을 써야 합니다. 비슷한 이유로 char, short, int를 받기 위해서는 va_arg(argp, int)를 써야 합니다. 덧붙여 질문 [*]11.3, [*]15.2도 참고하시기 바랍니다.

References
[C89] § 6.3.2.2
[ANSI Rationale] § 4.8.1.2
[H&S] § 11.4 p. 297



Q 15.11
함수 포인터를 va_arg()로 얻을려고 하는데, 잘 안됩니다.
Answer
typedef를 써서 함수 포인터를 새 타입으로 만들고 해 보기 바랍니다.

va_arg()와 같은 매크로는 함수 포인터와 같은 복잡한 타입을 사용할 때, 곤란을 겪기도 합니다 (be stymied). 이 문제를 이해하기 위해, 아래에 간단한 va_arg() 구현 예를 보입니다:

  #define va_arg(argp, type) \
    (*(type *)(((argp) += sizeof(type)) - sizeof(type)))
위에서, argp는 (즉, va_list 타입) char *입니다. 이 때, 만약 다음과 같이 불렀다면:
  va_arg(argp, int (*)())
va_arg는 다음과 같이 확장됩니다:
  (*(int (*)() *)(((argp) += sizeof(int (*)())) -
      sizeof(int (*)())))
위 결과를 자세히 보면 알겠지만, syntax error입니다. (첫번째 (int (*)() *)로 캐스트하는 것은 의미가 없습니다.)15.2

만약, 함수 포인터를 다른 이름으로 typedef했다면 모든 문제가 해결됩니다. 주어진 함수 포인터를 다음과 같이 typedef를 만들었다면:

  typedef int (*funcptr)();
그리고 아래와 같이 불렀다면:
  va_arg(argp, funcptr)
다음과 같이 확장됩니다:
  (*(funcptr *)(((argp) += sizeof(funcptr)) -
      sizeof(funcptr)))
이 경우, 정상적으로 동작합니다.

덧붙여 질문 [*]1.13, [*]1.17 [*]1.21도 참고하시기 바랍니다.

References
[ANSI] § 4.8.1.2
[C89] § 7.8.1.2
[ANSI Rationale] § 4.8.1.2

15.4 Harder Problems

You can pick apart variable-length argument lists at run time, as we've seen. But you can create them only at compile time. (We might say that strictly speaking, there are no truly variable-length argument lists; every actual argument list has some fixed number of arguments. A vararg function merely has the capability of accepting a different length of argument list with each call.) If you want to call a function with a list of arguments created on the fly at run time, you can't do so portably.



Q 15.12
가변 인자를 받아서 다시 가변 인자를 처리하는 함수에 넘겨 줄 수 있을까요?
Answer
일반적으로, 불가능합니다. 이상적으로, 이 경우, va_list를 받는 함수를 만들어야 합니다.

예를 들어, 치명적인(fatal) 에러 메시지를 출력하고 프로그램을 끝내는 함수인 faterror를 만든다고 가정해 봅시다. 여러분은 질문 [*]15.5에서 쓴 error 함수를 쓰길 원한다고 합시다:

  void faterror(char *fmt, ...)
  {
    error(fmt, ????);
    exit(EXIT_FAILURE);
  }
위에서 보면 알겠지만, faterror가 받은 가변 인자를 error에 전달할 방법이 없습니다.

이 문제를 처리하기 위해서, 첫째, error 함수를 분해해서 가변 인자 대신 하나의 va_list를 받는 verror 함수를 만듭니다. (별로 어렵지 않습니다. 왜냐하면 verror의 대부분은 error와 같으며, 일단 verror를 만들고 나면, error 함수는 verror를 써서 아주 쉽게 만들 수 있습니다.)

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

  void verror(char *fmt, va_list argp)
  {
    fprintf(stderr, "error: ");
    vfprintf(stderr, fmt, argp);
    fpritnf(stderr, "\n");
  }

  void error(char *fmt, ...)
  {
    va_list argp;
    va_start(argp, fmt);
    verror(fmt, argp);
    va_end(argp);
  }
위와 같이 만들었으면, 이제 faterror를 다음과 같이 만들 수 있습니다:
  void faterror(char *fmt, ...)
  {
    va_list argp;
    va_start(argp, fmt);
    verror(fmt, argp);
    va_end(argp);
    exit(EXIT_FAILURE);
  }
자세히 보면 errorverror의 관계는 printvprintf의 관계와 같습니다. Chris Torek씨가 제안한 것에 따르면, 여러분이 가변 인자를 처리하는 함수를 만들때마다, 두 가지 버전을 제공하는 것이 좋습니다: 하나는 (verror와 같이) va_list를 받아서 처리하는 함수와, (error와 같이) 앞 함수의 간단한 wrapper 역할을 하는 함수, 두 가지입니다. 이 방식의 한가지 단점은, verror와 같은 함수는 가변 인자를 단 한번씩만 scan할 수 있다는 것입니다; va_start를 다시 부를 방법은 없습니다.

만약, (faterror와 같은) 가변 인자를 받아서 이 것을 가변 인자를 받는 다른 함수에 전달하려고 하는데, (error와 같이) va_list를 받는 저수준 함수를 다시 만들 수 없다면, 이 문제를 해결하기 위한 portable한 방법은 존재하지 않습니다. (이 문제를 시스템의 어셈블리 언어를 써서 해결할 가능성은 있습니다. 덧붙여 질문 [*]15.13도 참고하시기 바랍니다.)

다음과 같은 방법은 동작하지 않습니다:

  void faterror(char *fmt, ...)
  {
    va_list argp;
    va_start(argp, fmt);
    error(fmt, argp);      /* WRONG */
    va_end(argp);
    exit(EXIT_FAILURE);
  }
va_list 자체는 가변 인자 리스트가 아닙니다; 내부적으로 이 것은 가변 인자 리스트를 가리키는 포인터입니다. 따라서 va_list를 받는 함수는 가변 인자 리스트를 받는 함수가 아니며, 그 역도 성립하지 않습니다.

Another kludge that is sometimes used and that sometimes works even though it is grossly nonportable is to use a lot of int arguments, hoping that there are enough of them and that they can somehow pass through pointer, floating-point, and other arguments as well:

  void faterror(fmt, a1, a2, a3, a4, a5, a6)
  char *fmt;
  int a1, a2, a3, a4, a5, a6;
  {
    error(fmt, a1, a2, a3, a4, a5, a6); /* VERY WRONG */
    exit(EXIT_FAILURE);
  }
위 예는, 이런 식으로 하지 말라는 것을 보여주기 위해 만든 것입니다. 이 글에서 봤다는 이유로 위 코드를 쓰려고 하지 말기 바랍니다.



Q 15.13
런타임에 인자 리스트를 만들어서 함수를 부를 수 있는 방법은 없을까요?
Answer
호환성있는 방법도 없으며, 그런 일을 할 수 있다고 보장할 수도 없습니다.

실제 인자 리스트를 처리하는 대신 generic (void *) 포인터의 배열을 넘겨주는 방법을 생각할 수 있습니다. 그리고 불려진 함수는 (main()argv 인자를 처리하듯이) 이 배열 요소를 하나씩 조사해서 원하는 정보를 얻을 수 있습니다. (물론, 이 방식은 여러분이 불려진 모든 함수를 직접 제어할 수 있을 때에만 의미가 있습니다.)

덧붙여 질문 [*]19.36도 참고하시기 바랍니다.

Seong-Kook Shin
2018-05-28