본문 바로가기

별걸다하는 IT/프로그래밍언어

[C,C++] 가변인자 매크로 (Variadic Macros) 알아보기. __VA_ARGS__, ##__VA_ARGS, ##args를 아시나요?

[C,C++ 프로그래밍 목차 링크 모음]

안녕하세요~~!! 양햄찌 주인장입니다. 저번 시간에 여러 인자를 넘겨 받을 수 있도록 가변인자(variable argument)사용하는 법을 배웠는데요. 이 주제에 이어서 가변인자 매크로에 대해 얘기해볼까 합니다. 

 

혹시 Variable Argument에 관한 내용을 모른다! 하시는 분들은 아래 포스팅을 먼저 읽고 오시면 이번편을 수월하게 이해하실 수 있습니다.

https://jhnyang.tistory.com/293

 

[C,C++] 가변인자 함수의 사용(va_start, va_arg, va_list등등) 함수에 불특정 여러개의 인자를 넘기고 싶�

[C, C++ 프로그래밍 강좌 목차] 안녕하세요~ 양햄찌 주인장입니다. 오늘은 오랜만에 프로그래밍 언어에 관련된 포스팅을 들고왔어요. 오늘의 주제 포스팅을 들어가기 전 'C++의 오버로딩'에 대한 ��

jhnyang.tistory.com

가변 인자가 무엇인가에 대한 부분은 알고 있다는 가정하에 진행하도록 할게용 :)

♣ 가변인자! 이제 쓸 줄 알아! 근데 가변매크로가 왜 필요해??

가장 많이 사용되는 대표적인 상황을 볼까요?

'어떤 변수에 무슨 값이 들어가있지/???' 이런거 확인하고 싶을 때 가장 쉽게 디버깅하는 방법은 바로, printf 함수를 써서 바로바로 찍어보는거잖아요. 개발할때야 툭하면 출력함수를 써서 이 값 저 값 확인하면서 개발을 진행하는데~ 

내가 개발을 다했어. 근데 내가 개발한게 다른 회사 서버랑 데이터를 주고받는 뭐 그런 프로그램이라고 해봅시다.

A회사가 어떤 데이터를 던져주면 그 데이터들 중 필요한 값을 뽑아서 쿵짝쿵짝 필요한 작업을 자동으로 하는 그런 프로그램이예요. 그런데 A회사에서 데이터를 아주 이상하게 보냈을 수 있겠죠. 하지만 얘가 데이터를 잘 줬는지 안줬는지 대체 우리가 어떻게 알죠?? 가장 좋은 방법은 프로그램이 실행될때마다 로그를 파일로 남기는겁니다.  

이를 위해 로그를 찍는 printlog를 만들어줄거예요 (물론 정확히 하려면 파일 write를 해줘야하지만, 간단히 볼게요) 

#include <stdio.h>
#include <stdarg.h> //가변인자를 위한 헤더파일~
void printlog_(const char* filename, const char* funcname, const int lineno, const char* fmt, ...);
void afunc(); void bfunc();
int main()
{
	afunc(); //a함수 실행, 여기서 a함수가 가진 데이터 및 로그정보를 찍어주고 싶어
	bfunc(); //b함수 실행, 여기서 b함수가 가진 데이터 및 로그 정보를 찍어주고 싶어
	return 0;
}
//실행중인 파일명, 함수명, 라인번호와 함께 개발자가 확인하고 싶은 데이터를 출력해주는 로그함수
void printlog_(const char* filename,const char* funcname, const int lineno, const char* fmt, ...) {
	va_list ap;
	va_start(ap, fmt);
	printf("[FILENAME: %s, FUNCNAME :%s, LINENO :%d] ", filename, funcname, lineno);
	vprintf(fmt, ap);
	va_end(ap);
}
void afunc()
{
	int num = 3;  //a함수가 가진 num값은 3
	printlog_(__FILE__, __FUNC__, __LINENO__, "num: %d\n", num); //~아 너무 길어
}
void bfunc()
{
	int num = 5; //b함수가 가진 num 값은 5
	printlog_(__FILE__, __FUNC__, __LINENO__, "num: %d\n", num);
}

이렇게 아주아주 간단한 레이아웃만 짜봤어요. __FILE__, __FUNC__, __LINENO__등의 매크로가 궁금하신 분은 '로그출력함수만들기 포스팅'을 참고해주세요.

 

그런데 __FILE__, __FUNC__, __LINENO__를 로그출력할때마다 일일이 쓰기엔 너무 번거롭지 않나요?? 그냥 우리가 쓰는 printf 처럼 로그출력함수도 사용할 수 있음 좋겠다라는 생각이 들거예요. 매크로로 넘겨줘서 좀 간단하게 호출부분을 만들어보려고 합니다. 그런데 우리 가변인수인 '...'을 썼는데 이걸 매크로로 그대로 넘겨도 될까?? 넘길 수 있을까? 얘는 어떻게 넘기지?

#include <stdio.h>
#include <stdarg.h>
#define printlog(fmt, ...) printlog_(__FILE__, __FUNCTION__, __LINE__, fmt, ##__VA_ARGS__)
void printlog_(const char* filename, const char* funcname, const int lineno, const char* fmt, ...);
void afunc(); void bfunc();
int main()
{
	afunc();
	bfunc();
	return 0;
}
void printlog_(const char* filename,const char* funcname, const int lineno, const char* fmt, ...) {
	va_list ap;
	va_start(ap, fmt);
	printf("[FILENAME: %s, FUNCNAME :%s, LINENO :%d] ", filename, funcname, lineno);
	vprintf(fmt, ap);
	va_end(ap);
}
void afunc()
{
	int num = 3;
	printlog("num: %d\n", num);
}
void bfunc()
{
	int num = 5;
	printlog("num: %d\n", num);
}

넵 넘길 수 있습니다. 위처럼 가변인수 매크로를 사용해주면 된답니당. afunc과 bfunc의 로그호출부분을 보면 훨씬 간단해졌죠?? 이와 같은 상황 뿐만 아니라 가변인수 함수를 써야할 일이 매우 많은데요.

매크로도 가변 개의 인수를 인자로 넘길 수 있는데 이때 사용하는 게 가변인자 매크로입니다.

♣ 가변인수 매크로 알기 - '__VA_ARGS__'와 '##__VA_ARGS__'

가변 인수 매크로는 __VA_ARGS__로 표현됩니다.

 

 

요렇게요! __VA_ARGS__가 줄임표'...'와 일치하는 인수로 대체됩니다. 이런 가변인수 매크로를 Variadic Macros라고 해요.

//사용예시들
#define CHECK1(x, ...) if (!(x)) { printf(__VA_ARGS__); }
#define CHECK2(...) { printf(__VA_ARGS__); }
#define eprintf(…)  fprintf(stderr, __VA_ARGS__)

잠깐! Variadic Macros?

 

 

 

Variadic 이란 단어는 사실 컴퓨터 용어입니다. 이 용어는 '가변적인'이라는 variable과 -adic이라는 접미사가 더해져 생긴 단어예요. 

-adic은 'having a certain number of arguments' 특정 개수의 인자를 의미할 때 붙여져요. 따라서 Variadic은 개수가 변할 수 있는 인자를 가지는걸 의미합니다. 


근데 얘가 문제가 하나 있어요.

printlog("num: %d\n", num);

이렇게 들어오면 위 사진의 경우, printlog_("num: %d\n", num) 즉 __VA_ARGS__부분에 'num'이 들어가니까 문제가 없는데, 

printlog("done!\n");

이렇게 들어오면 실제 확장은 printlog_("done!\n", ) 가 되어 문제가 생깁니다. 

 

에러~~~

 

printlog_("done!\n")가 아니라 printlog_("done!\n", )처럼 함수 내부가 컴마로 끝나면 gcc 컴파일러에서는 에러가 발생합니다. 즉 가변인수가 들어왔을 경우에는 괜찮은데, 가변인수가 한개도 들어오지 않으면 문제가 되는거예요.

(요새는 컴파일러들이 똑똑이라 알아서 자동적으로 처리해주는 경우도 물론 있습니다ㅎㅎ) 

 

이를 보완하고자 나온게 '##__VA_ARGS__'예요. 기존 '__VA_ARGS__'에다가 샾(#)두개를 붙여주면 마지막에 컴마로 끝나더라도 컴마를 삭제해버려 무시가 됩니다.  (참고로 더블샵 기호는 token paste operator라고 해요)

♣ 가변인수 매크로 알기 - (()), ##args

위에서 설명한 '__VA_ARGS__'와 '##_VA_ARGS__'는 C99에서 지정된 표준이예요. 이 매크로들은 그럼 표준 전에는 이런 방법이 없었나~? 필수적으로 이 기능을 써야 할 때가 분명 있었을 테니까 과거에도 이를 가능케 하는꼼수가 존재했습니다.

 

괄호 두개 (())

바로 괄호를 두개 사용하는건데요. 원리는 간단합니다.

 

 

이렇게 매크로를 정의하고 printlog를 호출할 때

 

 

이렇게 원하는 값을 'fmt'에 해당하는 부분에 넣어줍시다.

위 예시처럼 인자가 두 개인 ("num: %d\n", num)을 넣어줄 수도 있고 인자가 세 개인 ("num1: %d, num2: %d\n", num1, num2)를 넣어줄수도 있겠죠. 표준이 지정되기 전에는 이렇게 확장해서 사용했어요.

 

##args (named variable arguments)

Variadic macros are a new feature in C99. GNU CPP has supported them for a long time, but only with a named variable argument (args…, not … and __VA_ARGS__). 

__VA_ARGS__이런 형태의 가변매크로들은 C99에 등장한 녀석들입니다. 그 전에도 GNU CPP는 오랫동안 이 가변매크로와 유사한 기능을 수행할 수 있도록 지원해왔는데, '...'도 아니고 당근 그 이후에 표준으로 잡힌 __VA_ARGS__도 아니고, 바로 'args...'형태로 지원했습니다. 

#define printlog(fmt, args...) printlog_(fmt, ##args)

사용 방법은 가능합니다. 줄임말(...)부분 대신에는 'args...'을 '##__VA_ARGS__' 부분 대신에는'##args'를 써주시면 돼요. (##이 붙은 이유는 위에서 설명한 것과 동일!)

gnu에서만 가능해서 비주얼스튜디오에서는 적용 안됩니다. 리눅스에선 가능해요.

#include <stdio.h>
#include <stdarg.h>
#define printlog(fmt, args...) printlog_(fmt, ##args)
void printlog_(char* fmt, ...)
{
	va_list ap; va_start(ap, fmt); 
    vprintf(fmt, ap);
    va_end(ap);
}
int main()
{
	printlog("num:%d\n", 5); 
    return 0;
}   

참고로 꼭 'args'일 필요는 없어요. named variable arguments에서 유추할 수 있듯이 네이밍해주는 거기 때문에 

#define printlog(fmt, var...) printlog_(fmt, ##var)

요렇게 짝짝꿍만 잘 맞춰줘도 된답니다. 이 방법은 비표준이라 다른 컴파일러에서 동작되리란 보장이 없습니다. 

 

여담)

제가 이번에, 맡은 서비스 손좀 본답시고, '##__VA_ARGS__'를 활용해 소스를 좀 짰는데요.

서버에 대강 테스트로 짜서 돌려보니까 컴파일도 잘 되고 실행도 잘 되길래 기존 소스에다 삽입을 했습니다.

그런데...! 왠걸 컴파일이 안되는거예요. 하..핳ㅎ 맨 처음에는 make파일 문제인줄 알았죠. 

암튼, 말하고자 하려는 부분은, 컴파일러가 가변인수 매크로를 지원을 해줘도, 소스 환경은 워낙 무궁무진하여... 이게 컴파일시 어떤 옵션과 라이브러리에 얽혀있느냐에 따라 ##__VA_ARGS__가 표준이여도 먹히지 않을 수 있습니다.

특히나 금융권은 위험에 대해 민감해 보수적이기 때문에 아주 오래된 언어를 사용하기도 하고, 비효율적인 소스들이 녹아있어도 잘 변경하지 않습니다. 가변인수 매크로를 'args...' 형태로 변경해서 결국 에러 잡는데 성공했는데, 

GNU임이 확실하다면, 표준보다는 portable 을 생각해 'args...' & '##args'를 사용하는게 올바를 수 있어요. 상황에 맞게 잘 선별해서 쓰도록 합시다.

 

오늘 포스팅은 여기까지예요~!! 

오늘도 고생하셨습니다. 다음 포스팅에서 또 봐용~~! :)