Argument Prescan

Function-like 매크로에서 매크로 확장은 두 번 발생합니다. 첫 번째로 전달된 인수 값은 매크로 body 에서 치환되기 전에 먼저 완전히 확장됩니다. 이것을 argument prescan 이라고 합니다. 그다음 매크로 body 에서 해당 인수가 사용된 자리에서 확장된 값이 치환됩니다. 두 번째는 최종 확장된 결과에 대해서 다시 한번 매크로 확장이 발생하는데 이것을 main scan 이라고 합니다.

아래 예제를 보면 bar(foo) 매크로가 확장될 때 foo 매크로가 존재한다고 해서 bar(a,b) 로 확장된 후에 bar 매크로가 호출되는 것이 아닙니다 ( 그러면 인수 개수가 맞지 않아 bar 매크로에서 오류가 나겠죠 ). 또한 bar(foo) --> lose(foo) --> (1 + (foo)) --> (1 + (a,b)) 형태로 확장이 되는 것도 아닙니다.

  1. bar(x) 매크로에는 foo 하나의 인수로 전달되지만 이때 argument prescan 에의해 foo 매크로가 완전히 확장되어 a,b 가 됩니다.

  2. 그다음 x 매개변수가 사용된 자리에서 확장된 값이 치환되어 lose(x) ---> lose(a,b) 로 됩니다. ( 이때 만약에 x 매개변수에 # 또는 ## 연산자가 사용되면 그대로 foo 가 사용됩니다 ).

  3. 최종 확장된 결과에 대해서 main scan 이 발생하므로 lose(a,b) 매크로가 확장됩니다.

lose(x) 매크로는 하나의 인수만 가지므로 결과적으로 lose 매크로에서 오류가 발생하게 됩니다.

$ gcpp
#define foo  a,b
#define bar(x) lose(x)
#define lose(x) (1 + (x))

bar(foo)
@

<stdin>:5:8: error: macro "lose" passed 2 arguments, but takes just 1

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

# lose 매크로에서 오류가 나지 않게 하려면 다음과 같이 매개변수에 ( ) 를 추가해 주면 됩니다.
$ gcpp
#define foo  a,b
#define bar(x) lose((x))       // lose(x) ---> lose((x)) 로 변경하면
#define lose(x) (1 + (x))      // a,b 두개의 인수가 (a,b) 하나의 인수로 된다.

bar(foo)
@

(1 + ((a,b)))

## 연산자가 매크로의 매개변수 위치에 사용될 경우

다음을 보면 ## 연산자가 lose 매크로의 매개변수 위치에서 사용되고 있는데요. 이럴 경우 확장이 bar(2) ---> lose(foo_2) ---> lose(a,b) 형태로 되는 것이 아닙니다 ( 그러면 lose(x) 매크로와 인수 개수가 맞지 않아 오류가 나겠죠 ). 먼저 bar(2) 매크로가 확장이 되면 ## 연산자에 의해 최종 결과가 lose(foo_2) 가 되는데 여기서 main scan 이 일어나면 lose(foo_2) 이 상태에서 다시 매크로가 확장되는 것이지 lose(foo_2) ---> lose(a,b) 가 되는것이 아닙니다.

$ gcpp
#define foo_2  a,b
#define bar(x) lose(foo_ ## x)     // lose(foo_2) 상태에서 main scan 이 발생한다.
#define lose(x) (1 + (x))          // lose(x) 매크로에 foo_2 하나의 인수로 전달되고
                                   // 이후 argument prescan 에의해 a,b 로 확장되면
bar(2)                             // 최종 결과는 (1 + (a,b)) 가 된다.
@

(1 + (a,b))

예제 )

다음 예제는 function-like macros 메뉴에서 한번 소개된 예제인데 JOIN 매크로는 인수로 전달된 type 과 name 을 하나로 합치는 역할을 합니다. 먼저 매크로 인수 부분을 보면 TYPE 매크로가 사용된 것을 볼 수 있습니다. 인수에 사용된 매크로는 치환되기 전에 먼저 완전히 확장이 이루어지므로

TYPE(std::map<int, int>) ----> (std::map<int, int>) 가 되어 type 값으로 사용됩니다.

확장이 완료된 인수 값들을 이용해 JOIN 매크로가 확장되면 결과가

UNPACK (std::map<int, int>) map_var 가 되는데

자세히 살펴보면 UNPACK (std::map<int, int>) 부분이 UNPACK(...) 매크로 정의와 매칭이 됩니다 ( main scan ). 따라서 다시 매크로 확장이 발생하여 최종 결과가

std::map<int, int> map_var; 가 되게 됩니다.

$ gcpp
#define UNPACK(...) __VA_ARGS__
#define TYPE(...) ( __VA_ARGS__ )          // 전달된 인수에 괄호를 추가하는 매크로
#define JOIN( type, name ) UNPACK type name

JOIN( TYPE(std::map<int, int>), map_var );
@

std::map<int, int> map_var;

매크로 확장을 방지하는 방법

다음 FOO 매크로에 foo, bar 인수가 전달되면 ## 연산자에 의해 foobar () 로 확장이 되고 이후 main scan 에의해 foobar () 가 확장되어 최종 결과는 100 이 되게 됩니다. 만약에 main 확장을 방지하고 최종 결과가 foobar () 가 되게 하려면 어떻게 할까요?

gcpp
#define foobar() 100
#define FOO( name1, name2 ) name1 ## name2 ()

FOO( foo, bar )
@

100

이때는 매크로 body 가 empty 인 임의의 매크로를 선언하여 ( 여기서는 NOP() ), 매크로 이름과 괄호 사이에 위치시키면 됩니다.

$ gcpp
#define NOP()              // body 가 empty 인 매크로
#define foobar() 100
#define FOO( name1, name2 ) name1 ## name2 NOP() ()    // 이름과 괄호 사이에 위치시킨다.

FOO( foo, bar )
@

foobar ()                  // 매크로 확장이 일어나지 않는다.

위의 예제에서는 NOP() 매크로를 이용해 main 확장을 방지하여 최종 결과가 foobar () 가 되었는데요. 만약에 최종 결과인 foobar () 를 다시 확장시키려면 어떻게 할까요? 이때는 EVAL 과 같은 임의의 매크로를 정의하여 최종 결과를 한번더 확장시키면 됩니다. 그러면 최종 결과가 다시 100 이 되게 됩니다.

$ gcpp
#define NOP()
#define EVAL(...) __VA_ARGS__       // main scan 확장을 위한 EVAL 매크로 정의
#define foobar() 100
#define FOO( name1, name2 ) name1 ## name2 NOP() ()

EVAL( FOO( foo, bar ) )             // EVAL 매크로를 이용해 확장
@

100
------------------------------

1. EVAL 매크로의 인수가 되는 FOO(foo, bar) 는 1 개의 인수가 되는데 
   먼저 argument prescan 에의해 완전히 확장되면 foobar() 가 된다.
2. 확장된 결과는 __VA_ARGS__ 자리에 오게되고 그다음 main scan 이 발생하면
   100 으로 확장됩니다.
$ gcpp
#define EVAL(...) x __VA_ARGS__

EVAL( EVAL (EVAL (EVAL ( y ))))
@

x x x x y

#, ## 연산자는 인수확장이 적용되지 않습니다.

다음은 CONCAT 매크로를 이용해 foobar 를 만들고 QUOTE 매크로를 이용해 double quote 을 하려고 한 것인데요. 그런데 결과를 보면 CONCAT 매크로가 확장이 되지 않습니다. 이것은 QUOTE 매크로에서 사용된 #x 연산자에 의해 인수확장이 적용되지 않기 때문입니다.

$ gcpp
#define CONCAT(x) x ## bar
#define QUOTE(x) #x

QUOTE( CONCAT(foo) )
@

"CONCAT(foo)"

따라서 CONCAT 매크로를 확장 시키기 위해서는 QUOTE 매크로 확장을 방지해야 합니다.

$ gcpp
#define NOP()
#define CONCAT(x) x ## bar
#define QUOTE(x) #x

QUOTE NOP() ( CONCAT(foo) )
@

QUOTE ( foobar )

그런다음 결과에 EVAL 매크로를 적용시키면 CONCAT 과 QUOTE 매크로를 모두 확장시킬 수 있습니다.

$ gcpp
#define NOP()                  
#define EVAL(...) __VA_ARGS__
#define CONCAT(x) x ## bar
#define QUOTE(x) #x 

EVAL( QUOTE NOP() ( CONCAT(foo) ))
@

"foobar"
--------------------------------

1. EVAL 매크로의 인수 부분에 있는 QUOTE NOP() (CONCAT(foo)) 가 먼저
   argument prescan 에의해 완전히 확장되어 QUOTE(foobar) 가 된다.
2. 확장된 결과가 EVAL 매크로의 인수 값으로 사용되면 __VA_ARGS__ 자리에 
   QUOTE(foobar) 가 오게되고 그다음 main scan 이 발생하면 "foobar" 가 됩니다.

또는 QUOTE 매크로에 # 연산자가 사용되어 발생하는 문제이므로 다음과 같이 indirection 을 이용해도 CONCAT(foo) 매크로가 확장됩니다.

$ gcpp
#define CONCAT(x) x ## bar
#define QUOTE_(x) #x
#define QUOTE(x) QUOTE_(x)       // '#' 연산자가 없으므로 CONCAT(foo) 확장 결과가 사용된다.

QUOTE ( CONCAT(foo) )
@

"foobar"
---------------------------

1. QUOTE(CONCAT(foo)) 매크로의 인수 부분에 있는 CONCAT(foo) 매크로가 
   먼저 argument prescan 에의해 완전히 확장되어 foobar 가 된다.
2. QUOTE(x) 매크로의 매개변수 x 의 값이 foobar 로 치환되므로 QUOTE_(foobar) 가 된다.
2. QUOTE_(foobar) 매크로가 확장되면 #x 연산자에 의해 "foobar" 가 된다.

NOP() 매크로를 이용한 확장 방지는 다음과 같이 DEFER(...) 매크로를 정의해 사용할 수 있습니다.

$ gcpp
#define NOP()
#define DEFER(...) __VA_ARGS__ NOP()
#define EVAL(...) __VA_ARGS__
#define CONCAT(x) x ## bar
#define QUOTE(x) #x

DEFER(QUOTE) (CONCAT(foo))          // QUOTE NOP() 대신에 사용할 수 있다.
@

QUOTE (foobar)

Quiz

아래의 COUNT_ARGS(CCC(111,222,333)) 매크로 확장 결과로 전달되는 인수의 개수 3 을 기대하였지만 결과로 1 이 출력되고 있습니다. 이것은 COUNT_ARGS(X...) 매크로에서 사용된 ##X 연산자 때문에 CCC(111,222,333) 매크로 확장 결과가 사용되지 않기 때문인데요. 어떻게 하면 CCC(111,222,333) 확장 결과가 전달되어 인수의 개수가 3 이 출력되게 할 수 있을까요?

$ gcpp
#define __COUNT_ARGS(_0, _1, _2, _3, _4, _5, _n, X...) _n
#define COUNT_ARGS(X...) __COUNT_ARGS(, ##X, 5, 4, 3, 2, 1, 0) 
#define CCC(...) __VA_ARGS__        

COUNT_ARGS( CCC(111,222,333) )
@

1
----------------------------------

$ gcpp
#define __COUNT_ARGS(_0, _1, _2, _3, _4, _5, _n, X...) _n
#define COUNT_ARGS(X...) #X                                   
#define CCC(...) __VA_ARGS__

COUNT_ARGS( CCC(111,222,333) )
@

"CCC(111,222,333)"             // 다음과 같이 하나의 인수가 된다.

첫 번째 방법은 indirection 을 이용하는 방법입니다. 다음과 같이 호출 단계를 한 단계 더 추가하면 COUNT_ARGS(X) 매크로에서는 ## 연산자가 사용되지 않기 때문에 CCC(111,222,333) 매크로의 확장 결과가 사용될 수 있습니다. 매개변수 X 의 값이 확장 결과인 111,222,333 로 치환되면 _COUNT_ARGS(111,222,333) 가 되고 main scan 이 발생하면 확장 결과로 3 이되게 됩니다.

$ gcpp
#define __COUNT_ARGS(_0, _1, _2, _3, _4, _5, _n, X...) _n
#define _COUNT_ARGS(X...) __COUNT_ARGS(, ##X, 5, 4, 3, 2, 1, 0)
#define COUNT_ARGS(X) _COUNT_ARGS(X)
#define CCC(...) __VA_ARGS__

COUNT_ARGS( CCC(111,222,333) )
@

3

두 번째 방법은 다음과 같이 DEFEREVAL 매크로를 이용하는 방법입니다.

$ gcpp
#define NOP()
#define EVAL(...) __VA_ARGS__
#define DEFER(...) __VA_ARGS__ NOP()
#define __COUNT_ARGS(_0, _1, _2, _3, _4, _5, _n, X...) _n
#define COUNT_ARGS(X...) __COUNT_ARGS(, ##X, 5, 4, 3, 2, 1, 0)
#define CCC(...) __VA_ARGS__

EVAL( DEFER( COUNT_ARGS ) ( CCC(111,222,333) ) )
@

3

2 .

LISP 언어의 CAR, CDR, CONS 를 매크로로 구현하는 것입니다.

CAR 는 리스트에서 첫 번째 원소를 출력하고
CDR 은 리스트에서 첫 번째 원소를 제외한 나머지 원소들을 출력합니다.
CONS 는 첫 번째 인수로 주어진 원소를 두 번째 인수로 주어진 리스트에 병합합니다.

$ gcpp
#define EVAL(...) __VA_ARGS__
#define JOIN(X, Y) X ## Y
#define CAT(X, Y) JOIN(X, Y)               // JOIN 대신 CAT 을 사용하면 인수 확장이 됨.
#define IF_1(true, ...) true
#define IF_0(true, ...) __VA_ARGS__
#define IF(value) CAT(IF_, value)

#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 CAR(list) CAR_ list
#define CDR(list) CDR_ list
#define CAR_(X, ...) X
#define CDR_(X, ...) (__VA_ARGS__)
#define ISEMPTY(list) CHECK_EMPTY(CAR(list))
#define CONS(X, list) IF(ISEMPTY(list)) ((X), (X, EVAL list))

CAR( (33, 44, 55, 66) )
CDR( (33, 44, 55, 66) )
CONS( 22, (33, 44, 55, 66) )
CONS( 22, () )
@

33                           # CAR
(44, 55, 66)                 # CDR
(22, 33, 44, 55, 66)         # CONS
(22)                         # CONS