본문 바로가기
프로그래밍/C & C++

[C/C++] 가변 인자 리스트

by 별준 2022. 2. 23.

References

Contents

  • 가변 인자 리스트 (variable-length argument list)

Variable-Length Argument Lists

기존 C 언어의 기능인 가변 인자 리스트(variable-length argument list)를 살펴보겠습니다. 간혹 레거시 코드에서 사용하기도 하는데, 새로 C++로 작성한다면 가변 인자 템플릿(variadic template)을 사용하는 것이 좋습니다.

 

<cstdio>에서 제공하는 C 함수인 printf()를 먼저 살펴보겠습니다. 이 함수를 호출할 때 전달할 수 있는 인자는 다음과 같이 다양합니다.

printf("int %d\n", 5);
printf("String %s and int %d\n", "hello", 5);
printf("Many ints: %d, %d, %d, %d, %d\n", 1, 2, 3, 4, 5);

C/C++은 가변 길이의 인수를 가진 함수를 직접 정의하는 데 필요한 문법과 유틸리티 매크로를 제공합니다. 이렇게 정의한 함수는 위에서 본 printf()와 형태가 비슷합니다. 물론 이 기능이 필요할 때가 흔하지는 않지만 간혹 이 기능을 쓰면 편할 때가 있습니다. 예를 들어 디버그 플래그를 설정되었다면 stderr로 스트링을 출력하고 그렇지 않으면 아무 일도 하지 않는 디버깅 함수를 구현한다고 가정해봅시다. printf()처럼 이 함수도 인수의 개수와 타입을 임의로 지정할 수 있다면 편리할 것입니다.

간단하게 구현하면 다음과 같습니다.

#include <cstdio>
#include <cstdarg>

bool debug{ false };

void debugOut(const char* str, ...)
{
    va_list ap;
    if (debug) {
        va_start(ap, str);
        vfprintf(stderr, str, ap);
        va_end(ap);
    }
}

코드에서 먼저 살펴볼 것은 debugOut()의 프로토타입입니다. 이 부분을 보면 이름과 타입이 지정된 str이라는 매개변수 뒤에 생략 부호(...)이 있습니다. 이는 임의 개수와 타입의 인수를 받을 수 있다는 것을 의미합니다. 이렇게 선언된 인수는 반드시 <cstdarg>에 정의된 매크로로 접근해야 합니다. 함수 본문을 살펴보면 va_list 타입의 변수를 선언한 뒤 va_start를 호출해서 초기화했습니다. va_start()의 두 번째 매개변수는 반드시 매개변수 리스트의 이름 있는 변수 중에서 오른쪽 끝에 있는 것이어야 합니다. 즉, 가변 인자 리스트를 가진 함수라면 반드시 이름이 있는 매개변수가 한 개 이상 있어야 합니다. 여기서 정의한 debugOut() 함수는 인수 리스트를 곧바로 <cstdio>에 있는 표준 함수인 vfprintf()로 전달합니다. vfprintf()를 호출한 결과가 리턴되면 va_end()를 호출해서 가변 인자 리스트에 대한 접근을 종료합니다. va_start()를 호출했다면 반드시 이에 대응되는 va_end()를 호출해서 스택 상태를 일관성 있게 유지해야 합니다.

 

이렇게 정의한 debugOut() 함수를 사용하는 방법은 다음과 같습니다.

int main()
{
    debug = true;
    debugOut("int %d\n", 5);
    debugOut("String %s and int %d\n", "hello", 5);
    debugOut("Many ints: %d, %d, %d, %d, %d\n", 1, 2, 3, 4, 5);
}

 

Accessing the Arguments

실제 인자에 접근하고 싶다면 va_arg()를 사용하면 됩니다. 이 매크로는 va_list와 이를 해석할 타입을 인수로 받습니다. 하지만 인자 리스트의 끝이 무엇인지 명시적으로 지정하지 않고서는 알아낼 방법이 없습니다.

예를 들어, 첫 번째 매개변수를 매개변수 개수로 지정할 수 있습니다. 또는 포인터가 여러 개 있을 때 마지막 포인터를 nullptr로 지정하는 방법도 있습니다. 이 외에도 다양한 방법이 있지만 모두 프로그래머 입장에서 번거롭습니다.

 

다음 코드는 함수를 호출한 측에서 이름 있는 매개변수 중 첫 번째 매개변수로 인수 개수를 지정할 수 있게 구현하는 예를 보여줍니다. 이 함수는 임의 개수의 int 값을 받아서 출력합니다.

void printInts(size_t num, ...)
{
    int temp;
    va_list ap;
    va_start(ap, num);
    for (size_t i = 0; i < num; i++) {
        int temp{ va_arg(ap, int) };
        std::cout << temp << " ";
    }
    va_end(ap);
    std::cout << std::endl;
}

위 함수는 다음과 같이 호출할 수 있습니다.

printInts(5, 5, 4, 3, 2, 1);

 

 

이처럼 C 스타일의 가변 인자 리스트는 그리 안전하지 않습니다. 위 예제에서 살펴봤듯이 다음과 같은 문제들이 있습니다.

  • 매개변수의 개수를 알 수 없습니다. printInts() 예제를 보면 호출한 측이 첫 번째 인수에 정확한 개수를 입력하길 믿는 수 밖에 없습니다. debugOut()의 경우 문자 배열 뒤에 나올 인수 개수가 문자열에 담긴 포매팅 코드 개수와 일치한다고 믿는 수 밖에 없습니다.
  • 인수 타입을 알 수 없습니다. va_arg() 매크로는 전달된 타입을 이용하여 현재 시점의 값을 해석합니다. 그런데 va_arg()를 통해 얼마든지 그 값이 다른 타입으로 해석될 수도 있습니다. 따라서 타입이 정확한지 검증할 방법이 없습니다.

 

이처럼 C 스타일의 가변 인자 리스트는 가능하면 사용하지 않는 것이 좋습니다. 그 대신 값을 std::array나 std::vector에 담아서 전달하고, 그 값을 이니셜라이저 리스트로 초기화하거나 타입에 안전한 가변 인자 템플릿을 사용하는 것이 좋습니다.

댓글