Variadic Macros

매크로에서도 가변 인수를 사용할 수 있습니다. 매크로 매개변수에는 ... 로 가변 인수임을 표시하고 매크로 정의에는 __VA_ARGS__ 식별자를 사용하면 됩니다.

방법 1 : ...__VA_ARGS__ 를 사용

gcpp
#define FOO(...) __VA_ARGS__

FOO( aa, bb, cc )
@

aa, bb, cc
-------------------------------

$ gcpp
#define eprintf(...) fprintf (stderr, __VA_ARGS__)

eprintf ("%s:%d: ", input_file, lineno)
@

fprintf (stderr, "%s:%d: ", input_file, lineno)

방법 2 : name...name 를 사용

$ gcpp
#define FOO(args...) args       

FOO( aa, bb, cc )
@

aa, bb, cc
---------------------------

$ gcpp
#define eprintf(args...) fprintf (stderr, args)

eprintf ("%s:%d: ", input_file, lineno)
@

fprintf (stderr, "%s:%d: ", input_file, lineno)

인수의 개수

foo(a, b) 와 같이 2 개의 매개변수를 갖는 매크로가 있을 경우 매크로 호출 시 전달되는 인수가 매개변수의 개수와 맞지 않으면 오류가 됩니다. 예를 들어 foo(1) , foo(1,2,3) 는 오류가 됩니다. 하지만 variadic macro 의 경우는 ... 앞부분의 인수 개수만 맞으면 됩니다.

$ gcpp
#define foo(a, b, ...) a  b  __VA_ARGS__ 

foo(100, 200, 300)
foo(100, 200)                # 인수가 2 개일 경우도 오류가 되지 않는다.
@

100 200 300
100 200
----------------------------------------

$ gcpp
#define foo(a, b, ...) a  b  __VA_ARGS__ 

foo(100)                     # 인수가 1 개일 경우는 오류. (a, b 최소 2 개 이상이어야 한다.)
@
<stdin>:3:8: error: macro "foo" requires 3 arguments, but only 1 given
<stdin>:1: note: macro "foo" defined here

foo

아래 두 번째 foo() 매크로 호출의 경우 인수의 개수는 1 개로 오류가 되지 않습니다.

$ gcpp
#define foo(a) a

foo(100)
foo()              # 인수의 값은 empty 이고 개수는 1 개가 된다.
@

100
         # empty

, ##__VA_ARGS__

Variadic macros 를 사용할 때 발생하는 한 가지 문제점은 가변 인수 값으로 전달되는 인수가 empty 일 경우입니다. 이럴 경우 __VA_ARGS__ 값이 empty 가 되어 앞에 comma 가 남게 되므로 문법상 오류가 발생합니다.

다음과 같이 __VA_ARGS__ 값이 존재할 경우는 확장 결과가 오류가 되지 않지만

$ gcpp
#define eprintf(format, ...) fprintf (stderr, format, __VA_ARGS__)

eprintf ("%s:%d: ", input_file, lineno)
@

fprintf (stderr, "%s:%d: ", input_file, lineno)

format 스트링 값에 %s 와 같은 지정자를 사용하지 않아 __VA_ARGS__ 값이 empty 가 되면 확장 결과에 comma 가 남게 되어 문법상 오류가 됩니다.

$ gcpp
#define eprintf(format, ...) fprintf (stderr, format, __VA_ARGS__)

eprintf ("error occurred")
@

fprintf (stderr, "error occurred", )       # comma 가 남아 문법상 오류가 된다.

이와 같은 상황을 위해서 특별히 제공하는 기능이 , ##__VA_ARGS__ 입니다. __VA_ARGS__ 값이 empty 일 경우 ##__VA_ARGS__ 앞에 , 가 오면 , 가 제거됩니다.

$ gcpp
#define eprintf(format, ...) fprintf (stderr, format, ##__VA_ARGS__)

eprintf ("error occurred")
@

fprintf (stderr, "error occurred")         # comma 가 제거된다.

새로 추가된 기능으로 __VA_OPT__(arg) 매크로가 있는데 이 매크로는 __VA_ARGS__ 와 함께 사용되고 __VA_ARGS__ 값이 empty 이면 empty 가 반환되고 그렇지 않으면 arg 값이 반환됩니다.

$ gcpp
#define eprintf(format, ...) fprintf (stderr, format __VA_OPT__(,) __VA_ARGS__)

eprintf ("%s:%d: ", input_file, lineno)
@

fprintf (stderr, "%s:%d: " , input_file, lineno)

------------------------------------------------

$ gcpp
#define eprintf(format, ...) fprintf (stderr, format __VA_OPT__(,) __VA_ARGS__)

eprintf ("error occurred")
@

fprintf (stderr, "error occurred" )

매크로에서 반복 기능 구현하기

CPP 에서는 기본적으로 recursion 이 안되므로 다음과 같은 방식으로 반복 기능이 구현됩니다.

// linux/include/linux/syscalls.h 참조

$ gcpp
#define __MAP0(m,...)
#define __MAP1(m,t,a,...) m(t,a)
#define __MAP2(m,t,a,...) m(t,a), __MAP1(m,__VA_ARGS__)
#define __MAP3(m,t,a,...) m(t,a), __MAP2(m,__VA_ARGS__)
#define __MAP4(m,t,a,...) m(t,a), __MAP3(m,__VA_ARGS__)
#define __MAP5(m,t,a,...) m(t,a), __MAP4(m,__VA_ARGS__)
#define __MAP6(m,t,a,...) m(t,a), __MAP5(m,__VA_ARGS__)
#define __MAP(n,...) __MAP##n(__VA_ARGS__)

__MAP(2, FOO, aa, bb, cc, dd)               // 2 번 반복 
@

FOO(aa,bb), FOO(cc,dd)

-------------------------------------

__MAP(3, FOO, aa, bb, cc, dd, ee, ff)       // 3 번 반복
FOO(aa,bb), FOO(cc,dd), FOO(ee,ff)

__MAP(1, FOO, aa, bb, cc, dd, ee, ff)       // 1 번 반복
FOO(aa,bb)

__MAP(0, FOO, aa, bb, cc, dd, ee, ff)       // 0 번 반복
          // empty
__MAP(2, FOO, aa, bb, cc, dd)

1. __MAP(n,...) __MAP##n(__VA_ARGS__) 매크로의 첫 번째 인수는 반복 횟수가 되고 
   __MAP##n(__VA_ARGS__)  는  __MAP2(FOO, aa, bb, cc, dd ) 로 확장됩니다.

2. __MAP2(FOO, aa, bb, cc, dd ) 는 __MAP2(m,t,a,...) m(t,a), __MAP1(m,__VA_ARGS__) 
    매크로 정의에 따라 FOO(aa,bb), __MAP1(FOO, cc, dd) 로 확장되고

3.  __MAP1(FOO, cc, dd) 는 __MAP1(m,t,a,...) m(t,a) 매크로 정의에 따라 FOO(cc,dd) 로 확장되어

4. 최종 결과는 FOO(aa,bb), FOO(cc,dd) 가 되게 됩니다.

Quiz

다음은 전달된 인수의 개수를 출력해 주는 매크로입니다. 12 개 까지만 셀 수 있는데 어떻게 동작하는 것일까요?

// linux/include/linux/kernel.h 참조

#define __COUNT_ARGS(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _n, X...) _n
#define COUNT_ARGS(X...) __COUNT_ARGS(, ##X, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
1. COUNT_ARGS(X...) 매크로에서 사용된 X... 와 X 는 variadic macro 방법 2 에 해당합니다.

2. 따라서 , ##X 는 , ##__VA_ARGS__  와 같은것 입니다.
   전달된 인수가 empty 일경우 , ##X 부분이 제거되어 , 12, 11 ... 로 시작하게 되고
   전달된 인수가 aa, bb, cc 라면 , aa, bb, cc, 12, 11 ... 형태가 됩니다.

3. __COUNT_ARGS 매크로로 인수들이 전부 전달되면 _n 매개변수 위치에서 
   인수 개수에 해당하는 번호가 매칭되게 됩니다.

4. 따라서 매크로 확장으로 _n 을 사용하면 결과가 인수 개수가 됩니다.


$ gcpp
#define __COUNT_ARGS(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _n, X...) _n
#define COUNT_ARGS(X...) __COUNT_ARGS(, ##X, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)

COUNT_ARGS(foo,bar,zoo)    // 인수 개수가 3
@

3     # 결과
-------------------------------------------

$ gcpp
#define __COUNT_ARGS(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _n, X...) _n
#define COUNT_ARGS(X...) __COUNT_ARGS(, ##X, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)

COUNT_ARGS()               // 인수 개수가 0
@

0     # 결과

인자 개수에 따라 다르게 동작하는 매크로 함수 : https://casterian.net/c_c++/macro-args.html

2.

현재 매크로가 정의되어 있는지는 #ifdef 지시자나 defined 연산자를 이용해 체크해볼 수 있는데요.
매크로 값이 empty 인지는 어떻게 체크해볼 수 있을까요?

< 매크로 foo 값이 empty 일 경우 >

$ gcpp
#define JOIN(x, y) x ## y
#define CHECK_N(x, n, ...) n
#define CHECK(...) CHECK_N(__VA_ARGS__, 0)
#define CHECK_EMPTY(x) CHECK(JOIN(CHECK_EMPTY_, x))
#define CHECK_EMPTY_   , 1

#define foo             // 매크로 값이 empty
CHECK_EMPTY(foo)
@

1                       // 결과
--------------------------------------------

1. 매크로 foo 의 값이 empty 이므로 CHECK_EMPTY(foo) ---> CHECK(JOIN(CHECK_EMPTY_,)) 가 된다.
2. 인수에 해당하는 JOIN(CHECK_EMPTY_, ) 매크로가 먼저 확장되면 CHECK_EMPTY_ 가 되고
   CHECK_EMPTY_ 매크로가 존재하므로 다시 확장되면 , 1 가 된다.
3. CHECK(...) 매크로에 전달되는 인수 값이 , 1 가 되므로 CHECK_N( , 1, 0 ) 가 된다.
4. CHECK_N 매크로 정의에 따라 두 번째 인수 값을 취하게 되면 결과는 1 이 된다.

< 매크로 foo 값이 존재할 경우 >

$ gcpp
#define JOIN(x, y) x ## y
#define CHECK_N(x, n, ...) n
#define CHECK(...) CHECK_N(__VA_ARGS__, 0)
#define CHECK_EMPTY(x) CHECK(JOIN(CHECK_EMPTY_, x))
#define CHECK_EMPTY_   , 1

#define foo 100         // 매크로 값이 100
CHECK_EMPTY(foo)
@

0                       // 결과
------------------------------------------

1. 매크로 foo 의 값이 100 이므로 CHECK_EMPTY(foo) ---> CHECK(JOIN(CHECK_EMPTY_,100)) 가 된다.
2. 인수에 해당하는 JOIN(CHECK_EMPTY_, 100) 매크로가 먼저 확장되면 CHECK_EMPTY_100 가 된다.
3. CHECK(...) 매크로에 전달되는 인수 값이 CHECK_EMPTY_100 가 되므로 
   CHECK_N( CHECK_EMPTY_100, 0 ) 로 확장된다.
4. CHECK_N 매크로 정의에 따라 두 번째 인수 값을 취하게 되면 결과는 0 이 된다.

3.

C++ 언어에서 타입 정보를 조회하고자 할때 다음의 get_type_name(), get_type_category() 매크로가 유용하게 사용됩니다.

#include <iostream>
template <typename T> constexpr auto type_name()
{
    std::string_view name, prefix, suffix;
#ifdef __clang__
    name = __PRETTY_FUNCTION__;
    prefix = "auto type_name() [T = ";
    suffix = "]";
#elif defined(__GNUC__)
    name = __PRETTY_FUNCTION__;
    prefix = "constexpr auto type_name() [with T = ";
    suffix = "]";
#endif
    name.remove_prefix(prefix.size());
    name.remove_suffix(suffix.size());
    return name;
}

#define get_type_name(T) type_name<T>()                    // type 값을 전달
#define get_type_category(V) type_name<decltype(V)>()      // value 값을 전달

template <typename T>
void print_type(auto msg, T&& arg)
{
    std::cout << msg << " T   : " << get_type_name(T) << '\n';
    std::cout << msg << " arg : " << get_type_category(arg) << '\n';
}

int main()
{
    int num = 100;
    print_type("num", num);
    print_type("100", 100);
}

-----------------------------------------------------

$ g++ -std=c++20 test.cpp

$ ./a.out 
num T   : int&         // lvalue num 의 타입 T 의값은 int&
num arg : int&         // lvalue num 의 arg 값의 타입은 int& 
100 T   : int          // rvalue 100 의 타입 T 의값은 int
100 arg : int&&        // rvalue 100 의 arg 값의 타입은 int&&

그런데 다음과 같이 전달되는 인수에 , 콤마가 포함될 경우는 2 개의 인수로 인식이 되어 오류가 발생하는데요.

. . .
int main()
{
    auto tup = std::make_tuple( 100, "hello", 3.14 );
    std::cout << get_type_name(std::tuple_element_t<1, decltype(tup)>) << '\n';
}

---------------------------------------------------

$ g++ test.cpp
t3.cpp:37:69: error: macro "get_type_name" passed 2 arguments, but takes just 1
   89 |     std::cout << get_type_name(std::tuple_element_t<1, decltype(t1)>) << '\n';
      |                                                                     ^

이때 다음과 같이 variadic macro 를 사용하면 문제를 해결할 수 있습니다.

#define get_type_name(T, ...) type_name<T, ##__VA_ARGS__>()
#define get_type_category(V, ...) type_name<decltype(V, ##__VA_ARGS__)>()