back

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

4달 전 작성

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


C99 에 와서 C90 이후 컴파일러 확장에 의해 제공되던 가변 인자 매크로를 표준화하는 작업에 들어가게 됩니다. 컴파일러 별로 제공하는 형태가 상이했기 때문에 표준화 위원회는 그중 하나를 선택하기 보다는 __VA_ARGS__ 라는 새로운 이름을 도입해 가변 인자 매크로를 표준화하기로 결정합니다. 표준에서 2개의 _ 로 시작하는 이름과 _ 와 대문자로 시작하는 이름은 순수하게 표준을 위해 예약하고 있습니다.

C99 7.1.3p1 Reserved Identifiers

All identifiers that begin with an underscore and either an uppercase letter or another underscore are always reserved for any use.

C99 7.1.3p1 예약된 명칭

밑줄 문자 이후에 대문자나 밑줄 문자가 따라오는 모든 명칭은 항상 예약되어 있습니다.

때문에 새로운 기능을 갖는 이름이나 키워드가 추가될 때는 사용자의 이름 공간(name space)을 침범하지 않기 위해 ___+대문자로 시작하는 이름을 사용하게 됩니다.

그 대표적인 예 중 하나가 C99 에 추가된 데이터형인 불린(boolean)입니다. C99 에서 불린 데이터형은 _Bool 이라는 조금 괴상한 이름으로 정의되어 있고 실제 선언시

_Bool b;

와 같은 식으로 선언하여 사용해야 합니다. 만약 C99 이전에 _Bool 이라는 이름을 자신만의 용도로 사용하던 프로그램은 C99 에서는 오동작하거나 컴파일이 안 될 것입니다. 하지만, 이미 오래 전에 _+대문자로 시작하는 이름은 표준이 나중을 위해 예약해 둔다고 엄포를 놓은 바 있기 떄문에 해당 프로그램이 먼저 약속을 져버린 것입니다. 그렇다해도 무려 표준에서 지원하는 데이터형의 이름이 _Bool 인 것은 아무래도 모양 빠지기 때문에 추가로 <stdbool.h> 이라는 헤더를 제공합니다.

만약 자신의 프로그램에서 bool 이라는 명칭을 다른 용도로 사용하지 않는다면 <stdbool.h> 를 추가하고 모양 빠지는 _Bool 대신 intchar 등과 급을 같이하는 bool 이라는 이름을 쓸 수 있습니다.

#include <stdbool.h>

bool b;

이는 <stdbool.h> 에서 bool_Bool 로 치환하도록 정의하고 있기 때문에 가능한 일입니다.

다시 본론으로 돌아가 표준이 정의한 가변 인자 매크로는 아래와 같은 형태로 만들 수 있습니다.

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

그리고 호출은 마치 가변 인자 함수를 사용하는 것처럼 알흠다운 형태로 가능합니다.

debug("failed to get data from %s:%d", name, vaule);

짠! 이제 모든 문제가 해결된 것처럼 보입니다. 하지만! 실제로는 그렇지 않습니다. 만약 아래와 같이 호출하면 어떤 결과가 나올까요?

debug("failed to process user data");

만약, debug() 가 가변 인자를 받는 실제 함수였다면 아무 문제 없는 호출입니다. 그리고, 가변 인자 매크로가 실함수처럼 printf("%s:%d" "failed to process user data" "\n", __FILE__, __LINE__) 으로 확장된다면 아무 문제가 없을 것입니다. 안타깝게도 표준은 가변 인자 부분에 최소한 1개의 인자라도 있어야 한다고 요구하고 있습니다. 때문에 저 코드는 인자 개수가 부족한 오류에 해당됩니다. 실제 beluga 로 컴파일한 결과(ISO C requires at least one argument for __VA_ARGS__ 경고)를 아래에서 확인해 보실 수 있습니다 - 현재 웹 드라이버는 C90 모드로 설정되어 있어 C90 에서는 가변 인자 매크로가 지원되지 않는다고 경고가 뜹니다만 무시하셔도 좋습니다.

#include <stdio.h>

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

void foo(void)
{
    debug("failed to process user data");
}

가변 인자로 최소한 하나의 인자라도 넣어줘야 하므로 C99 부터 지원되는 (하지만, 많은 C90 컴파일러가 지원하고 있는) 빈 인자(empty argument)를 넣어주면

debug("failed to process user data", );

전처리 결과가 아래와 같이 되어 전처리기는 무사히 넘어가도 컴파일 단계에서 문제가 됩니다 - 뒤따라오는 인자 없이 덩그러니 놓인 마지막 쉼표에 주목하시기 바랍니다.

printf("%s:%d:" "failed to process user data" "\n", __FILE__, __LINE__,);

이번에도 실제 컴파일 결과를 확인해 보실 수 있습니다.

#include <stdio.h>

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

void foo(void)
{
    debug("failed to process user data",);
}

이번에는 전처리기에서 나오던 메시지(ISO C requires at least one argument for __VA_ARGS__)는 사라졌으나 컴파일러에서 발생하는 오류(extra comma or missing argument)를 확인할 수 있습니다.

결국 가변 인자 매크로가 없던 시절 가변 인자를 쓰기 위해 트릭을 사용한 것처럼, 이번에는 이 문제를 해결하기 위한 트릭이 등장하기 시작합니다!

표준화 과정에서 이런 문제를 예상하지 못한 것이 절대 아닙니다 - 표준화 위원회에 있는 사람들이 바보는 아니죠. 표준화 과정에서 문제 제기도 있었고 해결 방법에 대한 여러 제안도 있었으나, 위원회는 저런 사용 방법 자체가 바람직하지 않다고 판단한 것입니다.

C99 Rationale p.102

While some implementations have used various notations or conventions to work around this problem, the Committee felt it better to avoid the problem altogether.

C99 Rationale 102쪽

일부 구현체가 이 문제를 우회하기 위해 다양한 표기법이나 관례를 사용해 왔으나, 위원회는 해당 문제를 처음부터 피하는 것이 낫다고 판단하였습니다.

여기서 "이 문제"("this problem")가 바로 위에서 소개해드린 문제를 의미합니다.

하지만, 실제 프로그래머 입장에서는 분명 필요한 경우가 있어 이와 관련된 질문이나 트릭을 소개하는 답변을 인터넷에서 쉽게 찾을 수 있습니다. 사실, "트릭"이라는 이름답게 별로 바람직하지 않은 구조라 소개하고 싶지 않지만 궁금해 하는 분들을 위해 다음 글에서 간략히(?) 설명하고자 합니다.

넘어가기 전에 보너스로 gcc 가 표준 방식 이외에 제공하는 가변 인자 매크로 사용 방법을 소개해 드리겠습니다 - 이 방식을 표준화 위원회에 제안하였으나 거절된 바 있습니다. 사실, 이미 __VA_ARGS__ 를 사용하는 표준 방식이 많이 자리를 잡은 이후라 C99 이전에 gcc 용으로 작성된 과거 코드를 제외하면 크게 의미가 없는 방식일 수 있으나 알아서 남 주는 건 아니니...

#define debug(msg, ...data) printf("%s:%d:" msg "\n", __FILE__, __LINE__, data)

저 위의 표준 방식과 차이가 보이시나요? gcc 는 가변 인자에 __VA_ARGS__ 라는 이름 대신 프로그래머가 이름을 붙일 수 있도록 하고 있습니다. 이름은 ... 뒤에 따라오며 예에서 저는 data 라는 이름을 사용했습니다. gcc 방식의 장점이라면 가변 인자에 공통된 이름(__VA_ARGS__) 대신 의미 있는 이름을 부여할 수 있다는 것입니다. 하지만, 실 함수의 가변 인자를 지원하는 방식(...va_ 매크로) 비교하면 표준의 방식이 보다 일관성 있어 보이며, 의미 있는 이름을 부여하는 것이 일관성을 해치는 것보다 큰 가치를 준다고 말하긴 어려워 보입니다 - 그래서 거절됐겠죠?