back

가변 인자 매크로의 모든 것 - 개론

일 년 전 작성

이 글은 시리즈 글의 일부입니다.

수정: 마지막 글(가변 인자 매크로의 모든 것 - 보충)이 추가되었습니다.


variadic 이라는 단어를 아시나요? (인터넷을 포함해) 가까이 있는 영어 사전을 뒤적여도 답은 보이지 않습니다. 왜냐하면 컴퓨터 분야에서 만든 조합어이기 때문입니다.

variadicvariable 에 접미사 -adic 을 붙여 만든 단어입니다. 접미사 -adic 은 우리가 흔히 아는 접미사인 -ary 와 같은 의미입니다. 단항 연산자를 unary operator, 이항 연산자를 binary operator 라고 부르는데 이때 unary, binary 의 접미사 -ary 는 "-개의 인자나 피연산자를 갖는"이라는 뜻입니다. 그러니 varidic 은 variable(가변) 개수의 인자(혹은 피연산자)를 갖는다는 뜻이 되겠지요.

variadic 은 프로그래밍 언어에서 함수가 고정되지 않은 개수의 인자(argument)를 받을 때 붙이는 이름입니다. 우리 말로는 가변 인자 함수, 영어로는 variadic function 이라고 부르죠.

C 언어는 동적으로 인자를 구성하는 것을 허락하지는 않습니다. 함수가 가변 개수의 인자를 받는 것과 인자를 동적으로 구성할 수 있도록 해주는 것에는 차이가 있습니다. 천천히 생각해보면 가변 개수로 인자를 받는 경우에도 컴파일 시점에는 각 함수 호출의 인자 개수는 고정되어 있음을 알 수 있습니다. 우리가 흔히 아는 가변 인자 함수인 printf 도 실제 호출하는 모습을 보면

printf("%d * %d = %d", x, y, x * y);

실행해보지 않아도 인자 개수(문자열 포함하여 4개)를 세는데 어려움이 없습니다.

반면, JavaScript 같은 언어는 아래처럼 동적으로 함수 인자를 구성할 수 있도록 해줍니다.

function foo(p1, p2, p3, p4) {
    p1 && console.log(p1)
    p2 && console.log(p2)
    p3 && console.log(p3)
    p4 && console.log(p4)
}

var n = Math.floor(Math.random() * 4),
    args = []

for (var i = 0; i < n; i++)
    args.push(i+1)

foo.apply(null, args)

이 예에서는 실행 중 랜덤하게 인자 개수를 정해 foo() 에 전달하기 때문에 실행을 해야만 인자 개수가 결정됩니다 - C 에서도 배열과 요소 개수를 인자로 전달해 비슷한 효과를 낼 수는 있으나, 언어가 지원해주는 것과 흉내내는 것에는 분명한 차이가 있습니다.

가변 인자 함수를 만들고 사용하는 방법은 첫 C 표준인 C90 이 표준화되던 시절 컴파일러마다 다른 방식으로 지원되고 있던 내용을 정리해 표준안으로 정립하였습니다. C 언어 표준화의 목표는 새로운 언어 기능(feature)을 개발해 추가하는 것이 아니라 기존 쓰이는 기능(existing practice)을 정리(codify)하는 것이었습니다. 당시 가변 인자를 받는 매크로 함수("매크로 함수"의 표준 내 이름은 function-like macro 입니다)에 대한 사례가 부족했기에 이 부분에 대한 표준은 따로 마련되지 않았습니다.

매크로 함수의 주요한 기능 중 하나가 실 함수를 매크로로 감싸 숨기면서 함수 호출 비용(overhead)을 없애 성능은 유지하고 프로그래밍 편의를 도모하는 것입니다. 하지만, 매크로 함수는 가변 인자를 받을 수 있는 방법이 없어 가변 인자 함수는 존재하지만 이를 감쌀(wrapping) 수 있는 매크로는 만들 수 없게 되었습니다. 예를 들면, 아래와 같은 debug() 매크로를 만들 수 없게 된 것입니다.

#ifndef NDEBUG
#define debug(msg, 가변인자) printf("%s:%d:" msg "\n", __FILE__, __LINE__, 가변인자)
#else
#define debug(msg, 가변인자) ((void)0)
#endif

이 (가상) 코드의 의도는

debug("value from %s is %d", name, value);

이렇게 함수처럼 debug() 매크로를 호출해주면 인접 문자열 연결을 통해 아래와 같은 최종 형태가 나오는 것입니다.

printf("%s:%d:value from %s is %d\n", __FILE__, __LINE__, name, value);

물론, 표준이 가변 인자 함수를 만드는 표준 방법을 제공하고, printf 함수는 <stdarg.h>va_list 를 통해 인자를 받는 vprintf 를 제공하기 때문에, debug 라는 이름의 가변 인자 함수를 만들고 내부에서 vprintf 를 적절히 호출하면 원하는 바를 이룰 수 있습니다. 하지만, 저 위의 예에서는 디버그 모드가 아니라면 debug() 호출이 컴파일 시에 사라져 함수 호출 비용이 사라진다는 장점이 있으나, 매크로 대신 실 함수로 만들 경우 무의미한 함수 호출로 인한 비용 제거를 컴파일러 최적화에 맡긴다는 단점을 갖게 됩니다 - 그리고, 많은 컴파일러가 보통 함수 호출을 쉽게 제거하지 않으며, 특히 debug 함수의 정의와 호출이 서로 다른 파일에 있는 경우 함수 호출이 제거되기를 기대하기는 어렵습니다.

또한, 라이브러리를 설계하고 구현하는 입장에서 printf 같은 가변 인자 함수를 제공할 때, 사용하는 쪽에서 다른 이름으로 감싸서 사용할 수 있도록 va_list 를 받는 버전을 추가로 제공해야 하는 부담이 생깁니다. 만약, 표준 가변 인자 매크로가 제공되었다면 간단한 형태는 매크로로 감싸는 것이 가능해져 그런 부담이 줄게 됩니다.

이렇게 가변 인자가 가능한 함수와 가변 인자가 불가능한 매크로 사이에 간극이 발생하고, 표준 발표 이후 이 간극을 메우기 위해 표준 안에서는 여러가지 트릭이, 표준 밖에서는 컴파일러 별로 확장이 생기게 됩니다. 표준 밖 컴파일러 확장은 표준에서 정해준 가변 인자 매크로가 생긴 현 시점에서는 큰 의미가 없으므로 표준 안에서 사용했던 트릭부터 다음 글에서 살펴보고자 합니다.