back

가변 인자 매크로의 모든 것 - 현재 #2

7년 전 작성 | 6년 전 수정

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

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


이전 글에서 소개한 표준 가변 인자 매크로의 문제를 해결하기 위해서 가장 필요한 트릭은 매크로에 주어진 인자 개수를 세는 것입니다. 다시 말해, debug() 매크로에 주어진 인자 개수가 1개인 경우와 2개 이상인 경우를 나눌 수 있다면 표준이 정한 규칙 내에서 처리가 가능합니다. 예를 들어,

#define debug2(msg, ...) printf("%s:%d:" msg "\n", __FILE__, __LINE__, __VA_ARGS__)
#define debug1(msg)      printf("%s:%d:" msg "\n", __FILE__, __LINE__)

이렇게 정의해두고 이제 할 일은 debug 매크로에서 인자 개수를 조사해 두 매크로 중 하나로 보내면 문제가 해결됩니다. 하지만, 전처리기에는 if 와 같은 분기문이 없습니다. 그리고, 당연히 인자 개수를 셀 수 있는 방법도 제공되지 않습니다. 이 부분에 바로 트릭을 사용해야 합니다.

가변 인자 매크로를 사용해 주어진 인자 개수를 세는 트릭은 comp.std.c 뉴스 그룹소개된 바 있습니다 - 여기서는 설명을 위해 원래 예를 보다 간단히 수정하였습니다.

#define PP_NARG( ...) PP_NARG_(__VA_ARGS__, PP_RSEQ_N())
#define PP_NARG_(...) PP_ARG_N(__VA_ARGS__)
#define PP_ARG_N(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, N, ...) N
#define PP_RSEQ_N() 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0

PP_NARG() 는 자신에게 주어진 인자를 세어 개수를 반환해주는 매크로입니다. 예를 들어,

PP_NARG(A)
PP_NARG(A,B)
PP_NARG(A,B,C)
PP_NARG(A,B,C,D)
PP_NARG(A,B,C,D,E)

이렇게 호출하면 결과는

1
2
3
4
5

가 됩니다.

동작 원리를 따져보겠습니다. PP_NARG() 는 인자로 가변 인자만 받는 매크로 함수입니다. 가변 인자를 받는 실 함수는 최소한 1개의 고정된 인자를 받아야 하지만(고정된 인자의 위치를 바탕으로 따라오는 가변 인자에 접근할 수 있기 때문입니다), 매크로 함수에는 그런 제한이 없습니다. 단 이전 글에서 강조했듯이 가변 인자 부분에 최소 1개 이상의 인자가 주어져야 하는 것은 그대로 입니다.

PP_NARG(1개 이상의 인자) 로 호출하게 되면 PP_NARG() 는 다시 비슷한 이름의 PP_NARG_() 를 호출하게 됩니다. 이때 PP_NARGS_() 에는 이번에 받은 가변 인자와 뒤에 PP_RSEQ_N() 를 붙입니다. 모양으로 보면

PP_NARGS_(전달 받은 1개 이상의 인자, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)

가 됩니다. 이때 전달 받은 1개 이상의 인자 부분에 아래처럼 1개의 인자가 주어졌다고 가정하겠습니다.

PP_NARGS_(1개, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)

이제 PP_NARGS_() 의 확장 결과로 1개 뒤에 주어진 인자 중 1 이 선택되도록 하면 됩니다 - 앞에서부터 11번째 인자를 취하면 결과로 1 이 나오겠지요! 만약, 앞에 2개의 인자가 주어진 경우라면

PP_NARGS_(1개, 2개, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)

자연스럽게 11번째 인자를 취하면 이번에는 2 가 결과로 나오게 됩니다. 그럼 당연히 PP_NARGS_() 가 호출하는 매크로는 11번째 인자를 결과로 내는 매크로가 되겠지요. 물론, PP_NARGS_() 에 전달되는 인자 개수가 가변적이므로 마지막에 가변 인자를 받아 대응을 해야 합니다.

#define PP_ARG_N(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, N, ...) N

매개변수 이름이 좀 이상하게 생겼지만 기분탓입니다 밑줄로 시작하는 정상 이름입니다.

설명하는 과정에서 이 트릭의 한계 하나가 명확히 보입니다. 개수 검사를 위해 전달되는 인자 개수가 3개, 4개 계속 늘어나면 뒤따라오는 10, 9, ... 부분이 하나씩 밀려 11번째 인자는 3, 4, ... 로 커지게 됩니다. 그러다가 10 을 넘어가게 되면 더 이상 의미있는 개수가 아닌 검사를 위해 전달한 마지막 인자가 선택됩니다. 예를 들면, 아래 호출은

PP_NARG(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11)

A11 이라는 이상한(하지만 당연한) 결과를 내게 됩니다. 즉, 지금 소개하는 트릭은 인식할 수 있는 최대 인자 개수에 제한이 주어집니다. 물론, 함수나 매크로 함수에 사용하는 인자 개수가 극단적으로 많은 경우는 없기에 현실적인 제약이 되지는 않습니다. 더구나 비록 "고무 이빨(rubber teeth)"이지만 각 표준(C90, C99, C11) 별로 매크로 함수에 사용할 수 있는 최대 인자 개수를 명시해 주고 있으므로 그 정도만 지원해도 충분할 수 있습니다.

C99 5.2.4.1 Translation Limits

The implementation shall be able to translate and execute at least one program that contains at least one instance of every one of the following limits: ...

  • 127 parameters in one macro definition
  • 127 arguments in one macro invocation

C99 5.2.4.1 번역 제한

구현체는 각각의 다음 제한 각각을 모두 최소 하나씩 포함하고 있는 프로그램 최소한 하나를 번역하고 실행할 수 있어야 합니다. ...

  • 매크로 정의에 127개의 매개변수
  • 매크로 호출에 127개의 인자

하지만, 사실 처리할 수 있는 최대 인자 개수가 고정된다는 점 이외에도 쉽게 보이지 않는 문제가 숨어 있습니다. 그 문제를 살펴보기 전에 이번에 알게 된 트릭을 사용해 debug() 매크로를 직접 수정해 보겠습니다. 마음 같아선 127개의 인자까지 지원하고 싶지만 키보드 치기 귀찮아서 알아보기 힘드므로 계속 10개 정도로 제한해 보겠습니다.

앞서 PP_NARG() 매크로로 트릭을 사용해 매크로에 주어진 인자 개수를 파악하는 것까지는 문제가 없어 보입니다. 하지만, 여전히 전처리기에는 if 문처럼 분기를 처리할 수 있는 구조가 없는데 어떻게 인자 개수에 따라 분기를 할 수 있을까요? 아래에 답이 있습니다.

#define debug2(msg, ...) printf("%s:%d:" msg "\n", __FILE__, __LINE__, __VA_ARGS__)
#define debug1(msg)      printf("%s:%d:" msg "\n", __FILE__, __LINE__)

#define debug(...) xdebug(__VA_ARGS__)(__VA_ARGS__)
#define xdebug(...) debug_(__VA_ARGS__, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1)
#define debug_(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, N, ...) debug ## N

전처리기에서 분기는 보통 다른 행동을 하는 매크로를 이름을 달리하여 만들어 두고(우리 예에서는 debug1, debug2), 전처리기의 토큰 접합 연산자(token-pasting operator)## 를 사용해 구현합니다.

처음 PP_NARG() 예에서는 인자 개수를 결과로 내도록 했지만, 여기서는 인자 개수가 1개까지는 1, 그보다 많은 경우에는 2 가 나오도록 수정하고 이 숫자를 debug 에 붙여 debug1 혹은 debug2 가 호출되도록 합니다.

debug() 가 호출하는 xdebug(__VA_ARGS__) 부분이 앞서 소개한 트릭을 통해 debug1 혹은 debug2 를 만들고 뒤에 (__VA_ARGS__) 를 붙여 debug1 이나 debug2debug() 에 주어진 인자를 가지고 호출되도록 해줍니다.

이제 이렇게 수정된 코드가 진짜 오류 없이 동작하는지 확인해 보겠습니다.

#include <stdio.h>

#define debug2(msg, ...) printf("%s:%d:" msg "\n", __FILE__, __LINE__, __VA_ARGS__)
#define debug1(msg)      printf("%s:%d:" msg "\n", __FILE__, __LINE__)

#define debug(...) xdebug(__VA_ARGS__)(__VA_ARGS__)
#define xdebug(...) debug_(__VA_ARGS__, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1)
#define debug_(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, N, ...) debug ## N

void foo(void) {
    debug("only message");
}

이제 debug() 에 1개의 인자만을 전달해도 오류가 발생하지 않음을 알 수 있습니다. 이는 인자 개수 분기에 의해 debug2() 대신 debug1() 이 호출되어 나온 결과입니다.

갑자기 코드가 복잡해져서 혼란스러울 수 있겠지만, 원래 고민하던 문제로 다시 돌아오겠습니다. PP_NARG() 에서 인자를 하나도 주지 않으면 어떻게 될까요? 인자 개수를 반환할 때 사용되는 부분인 PP_RSEQ_N() 를 보면 0 이 포함되어 있으므로 느낌상으로는 0 이 잘 나올 것 같습니다. 만약 이 결과가 0 이 나온다면 아래 코드는 크기가 0인 배열을 선언하려는 코드가 되므로 오류가 나겠지요?

#define PP_NARG( ...) PP_NARG_(__VA_ARGS__, PP_RSEQ_N())
#define PP_NARG_(...) PP_ARG_N(__VA_ARGS__)
#define PP_ARG_N(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, N, ...) N
#define PP_RSEQ_N() 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0

void foo(void)
{
    int a[PP_NARG()];
}

하지만 결과는 아무 문제가 없습니다! 그 이야기는 인자를 하나도 주지 않은 PP_NARG() 의 호출 결과가 0 이 아니라는 뜻입니다. 이 부분이 앞서 소개한 트릭이 갖는 2번째 문제입니다.

해결책을 고민하기 전에 문제부터 제대로 이해할 필요가 있습니다. C99 가 매크로 함수에 빈 인자(empty argument)를 지원한다는 사실을 고려할 때, 아래 매크로 호출의 인자 개수는 몇 개일까요?

macro(,,)

쉼표 개수를 미루어 볼 때 3개가 될 것입니다. macro() 의 정의가 어떻게 생겼는지는 모르겠지만 3개의 비어 있는 인자를 전달하려는 코드입니다. 그럼 아래는 몇 개일까요?

macro()

비어 있는 인자 1개일까요? 아니면 0개일까요?

빈 인자를 지원함으로 해서 위와 같은 매크로 호출에서 인자 개수를 결정할 수 없는 모호성이 생기게 됩니다. 이때 인자 개수를 결정하기 위해서는 macro() 의 정의를 봐야만 합니다. 만약

#define macro(p)

로 정의되어 있다면 비어 있는 인자 1개가 되는 것이고,

#define macro()

로 정의되어 있다면 0개가 됩니다.

그리고, 표준에서 가변 인자 부분은 최소한 1개의 인자를 받아야 한다고 말씀드린 바 있습니다. 따라서,

PP_NARG()

는 0개의 인자가 아닌 빈 인자 1개를 전달하는 코드가 됩니다. 그러니 결과로 0 이 아닌 1 이 나오는 것이 지극히 자연스러운 결과가 됩니다. 보이지 않는 빈 인자 1개의 개수를 센 셈이죠.

PP_NARG() 가 사용하는 트릭은 최대 인자 개수에 제한이 있을 뿐 아니라 인자가 하나도 주어지지 않은 경우를 구분하지 못하는 단점을 갖습니다. 그럼 이제 추가로 인자가 주어지지 않은 경우(0개의 경우)를 구분하는 트릭이 등장합니다.

가변 인자 개수를 세는 트릭은 그래도 트릭 중에 "준수한" 편이지만, 인자가 주어지지 않은 경우를 구분하는 트릭은 심히 냄새가 납니다. 때문에 전체 트릭을 설명하진 않고 전처리기의 어떤 특성을 트릭에 활용했는지만 설명하고자 합니다.

매크로 함수가 호출되기 위해서는 매크로 함수 이름의 다음 토큰으로(사이에 공백이 있어도 무방합니다) 여는 괄호 ( 가 나와야 합니다. 따라서, 아래 코드에서

#define bar() empty
#define foo(...) bar __VA_ARGS__ ()

foo() 가 인자 없이 호출되어야 bar() 가 호출되고 empty 라는 결과가 나오게 됩니다.

물론, foo(bar) 처럼 foo() 에 주어진 인자가 bar 로 끝나는 경우, 분명 인자를 가지고 있음에도 bar() 가 확장되겠지요? 이제 그런 경우들을 배제하기 위해 코드가 복잡해지기 시작합니다. 하지만, 기본 원리 자체는 지금까지 설명한 부분에서 벗어나지 않습니다 - 궁금하신 분은 위에 링크된 글을 참고하시면 됩니다.

한참동안 골치 아프게 하는 트릭에 대해 설명했지만, 이렇게 복잡한 트릭을 도입해야 할 정도로 필요성이 있는 기능이라면 컴파일러가 확장으로 관련 기능을 지원했을 법도 합니다. 이제 다음 글에서 관련된 컴파일러 확장을 알아보도록 하겠습니다.