back

가변 인자 매크로의 모든 것 - 과거

4달 전 작성

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


이번 글에서는 이전 글에서 살펴본 예를 이용해 어떻게 가변 인자 매크로를 흉내낼 수 있는지 C90 시절 쓰이던 트릭에 대해서 살펴보고자 합니다. 사실, C11 은 몰라도 C99 가 분명히 자리를 잡은 시점에서 C90 시절 쓰인 트릭을 살펴보는 것이 쓸모 없어 보일 수도 있습니다. 하지만, 언어의 역사만큼 오래된 코드를 만났을 때 의도를 몰라(의도가 바로 이해된다면 "트릭"이 아니겠지요) 고개를 갸우뚱 거릴 수도 있고, 저같은 노인네는 아직도 C90 표준 내에서 코딩하는 것을 즐기는 편이라 C90 코드를 양산해내고 있습니다 (죄송합니다).

우선 보다 간단한 첫번째 트릭부터 소개해 보겠습니다.

#ifndef NDEBUG
#define debug(p) printf p
#else
#define debug(p) ((void)0)
#endif

코드만 보면 이 코드가 어떻게 가변 인자를 흉내내는 것인지 알기 어렵지만 호출하는 모양을 보면 바로 보입니다.

debug(("failed to process %s\n", name));

이렇게 사용하면, debug() 에 주어진 인자 ("failed to process %s\n", name) 가 매개변수 p 로 전달되어

printf ("failed to process %s\n", name);

요렇게 확장되어 동작합니다!

매크로 함수 호출을 인식할 때 인자를 분리하는 쉼표는 가장 바깥쪽에서만 인식됩니다. 다시 말해, 중복된 괄호로 둘러싸인 부분에 쉼표가 있다면 인자 분리로 인식되지 않고 매크로 함수의 인자를 구성하는 내용으로 인식합니다. 이런 행동은 실 함수에서도 유사하게 확인할 수 있습니다. 예를 들어,

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

와 같은 함수 호출은 4개의 인자를 전달하며 그 중 세번째 인자는 (3, 4) 라는 쉼표 연산자를 포함한 수식을 평가한 결과가 됩니다 - 물론 쉼표 연산자는 우측 피연산자만 값으로 취하기 때문에 첫번째 3 은 무의미한 값이 되고, 대부분의 컴파일러에서 값이 무시된다고 경고해 줄 것입니다.

이때 주의할 점은 매크로 함수 호출에서 인자 분리를 막는 괄호는 소괄호, (, ) 에만 해당된다는 사실입니다.

debug(["DEBUG: failed to process %s\n", name]);

와 같은 호출은 ["DEBUG: failed to process %s\n"name] 의 인자 2개로 인식됩니다.

이렇게 중복 괄호를 사용하는 방법은 코드 모양면에서 괄호를 중복으로 쓰긴 하지만 겉모양이 원래의 매크로 호출과 비슷하다는 장점이 있습니다.

다른 방법은 매크로가 치환되어 나온 결과를 전처리기가 추가로 스캔한다는 점을 이용하는 방법입니다.

#define _ ,
#define debug(p) printf(p)

이제 아래와 같이 호출해주면

debug("DEBUG: failed to process %s\n" _ name);

"DEBUG: failed to process %s\n", _ name 이라는 인자 하나가 전달되고 debug() 의 확장 결과는

printf("DEBUG: failed to process %s\n" _ name);

가 됩니다. 이 결과를 전처리기가 규칙에 따라 추가적인 매크로 확장을 위해 재스캔(rescan)하게 되고, 그럼 이제 매크로 이름 _ 가 보이기 때문에 이 부분이 쉼표로 치환되어 원하는 결과인

printf("DEBUG: failed to process %s\n" , name);

가 나오게 됩니다 - _ 가 유효한 이름(identifier)임에 유의하시기 바랍니다. C 언에서 명칭 (혹은 이름)은 숫자가 아닌 문자로 시작하며, 이때 문자에는 _ 도 포함되어 있습니다.

이 방법은 쉼표 대신 _ 를 써서 인자를 분리하기 때문에 모양이 별로이기는 합니다. 물론 _ 가 맘에 들지 않는다면 명칭에 해당하는 어떤 이름도 쓸 수 있습니다. 하지만,

#define comma ,
debug("failed to process %s\n" comma name);

이렇게 쓰면 오히려 가독성이 더 떨어지겠지요?

괄호를 두 번 써 주어야 한다는 것을 잊지만 않는다면 외관상 전자가 훨씬 더 자연스러워 보이는 것이 사실입니다. 또한, 실수로 괄호를 한번만 쓴다고 해서 예상하지 못한 방향으로 오동작하는 경우보다 전처리 과정에서 오류가 날 것이기 때문에 실수를 잡아내기도 쉽습니다. _ 를 쉼표로 대체하는 트릭은 실수로 _ 를 빼먹을 경우 전처리 과정에서 잡히지 않고 컴파일 과정까지 가서 오류를 낼 가능성이 큽니다.

이제 두번째 방식은 아무 짝에도 쓸모가 없어 보이는 방식 같지만, 사실 또 나름 유용한 경우가 있습니다. 바로 가변 인자로 구현하려는 매크로가 인자를 구분해서 취해야 하은 경우입니다. 위에서는 설명을 위해 debug() 매크로의 정의를 이전 글에서보다 간단하게 바꿨습니다. 다시 처음 글에서 보였던 예로 돌아가면 이렇게 생겼습니다.

#define debug(msg, 가변인자) printf("%s:%d:" msg "\n", __FILE__, __LINE__, 가변인자)

이때는 중복된 괄호를 사용하는 방법으로는 원하는 결과를 얻을 수 없습니다. 중복 괄호 트릭은 반드시 인자 전체가 그대로 실 함수로 전달될 때에만 가능합니다. 하지만, 쉼표를 _ 로 바꿔쓰는 방식은 필요한 인자를 취하는 행동을 구현할 수 있습니다.

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

이렇게 호출하면

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

요렇게 확장되고, 인접 문자열 연결과 매크로 재스캔에 의해 최종적으로 아래 결과가 나옵니다.

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

저는 개인적으로 _ 트릭을 즐겨 사용하는 편이며 매크로를 사용해 테이블 초기치를 제공할 때 유용하게 쓰고 있습니다. 한 가지 예로 beluga 에서 타깃 머신(target machine)을 기술하는 파일을 비롯해 여러 곳에서 열심히 사용하고 있습니다.

이제 과거의 이야기를 나눴으니 현재 가변 인자 매크로가 표준에서 어떻게 지원되고 있는지 살펴볼 차례입니다. 다음 글에서 만나보도록 하겠습니다.