programing

매크로가 간접적으로 확장될 때 C의 프리프로세서의 동작을 이해한다.

prostudy 2022. 7. 1. 20:43
반응형

매크로가 간접적으로 확장될 때 C의 프리프로세서의 동작을 이해한다.

매크로 트릭과 마법으로 가득 찬 큰 프로젝트를 진행하던 중 매크로가 제대로 확장되지 않는 버그를 우연히 발견했습니다.결과 출력은 다음과 같습니다.EXPAND(0)"그런데EXPAND"로 정의되었습니다.#define EXPAND(X) X"그러니까 분명히 출력은 " "로 되어있어야 했다.0".

"괜찮아."라고 나는 생각했다."어쩌면 바보 같은 실수일 수도 있어요. 결국 여기엔 나쁜 매크로가 몇 개 있어요. 잘못될 곳이 많으니까요.그래서 저는 잘못된 행동을 하는 매크로를 200라인 정도의 자체 프로젝트로 분리하여 문제를 특정하기 위한 MWE 작업을 시작했습니다.200줄은 150줄, 100줄은 20줄, 10줄은...정말 충격적이게도 이게 내 마지막 MWE였어

#define EXPAND(X) X
#define PARENTHESIS() ()
#define TEST() EXPAND(0)
   
EXPAND(TEST PARENTHESIS()) // EXPAND(0)

줄.

엎친 데 덮친 격으로 매크로를 거의 수정하면 매크로가 올바르게 동작합니다.

#define EXPAND(X) X
#define PARENTHESIS() ()
#define TEST() EXPAND(0)

// Manually replaced PARENTHESIS()
EXPAND(TEST ()) // 0
#define EXPAND(X) X
#define PARENTHESIS() ()
#define TEST() EXPAND(0)

// Manually replaced TEST()
EXPAND(EXPAND(0)) // 0
// Set EXPAND to 0 instead of X
#define EXPAND(X) 0
#define PARENTHESIS() ()
#define TEST() EXPAND(0)

EXPAND(TEST PARENTHESIS()) // 0

그러나 가장 중요한 것은 아래 코드도 동일한 방식으로 실패한다는 것입니다.

#define EXPAND(X) X
#define PARENTHESIS() ()
#define TEST() EXPAND(0)
   
EXPAND(EXPAND(EXPAND(EXPAND(TEST PARENTHESIS())))) // EXPAND(0)

이는 프리프로세서가 완벽하게 확장 가능하다는 것을 의미합니다.EXPAND하지만 어떤 이유에서인지, 마지막 단계에서 다시 확장하기를 절대 거부합니다.

실제 프로그램에서 이 문제를 어떻게 해결할지는 중요하지 않습니다.솔루션도 좋지만(즉, 토큰을 확장하는 방법)EXPAND(TEST PARENTHESIS())로.0가장 관심이 있는 것은, 「왜?」입니다.C 프리프로세서는 왜 다음과 같은 결론을 내리게 되었습니까?EXPAND(0)첫 번째 케이스에서는 올바른 확장이었지만 다른 케이스에서는 그렇지 않았습니까?

C 프리프로세서의 기능(및 그 기능을 사용할 수 있는 마술)에 관한 리소스는 쉽게 찾을 수 있지만, 아직 그 기능을 설명하는 리소스를 찾을 수 없습니다.이 기회에 프리프로세서가 어떻게 작업을 수행하고 매크로를 확장할 때 어떤 규칙을 사용하는지 더 잘 이해하고 싶습니다.

그 점에 비추어 보면:프리프로세서가 최종 매크로를 ""로 확장하기로 결정한 이유는 무엇입니까?EXPAND(0)"가 아니라 ".0"?


편집: Chris Dodd의 매우 상세하고 논리적이고 훌륭한 답변을 읽은 후, 저는 같은 상황에서 누구나 할 수 있는 일을 했습니다.반례를 생각해 내다:)

제가 만든 건 다른 4라인이에요

#define EXPAND(X) X
#define GLUE(X,Y) X Y
#define MACRO() GLUE(A,B)

EXPAND(GLUE(MACRO, ())) // GLUE(A,B)

이제, C 프리프로세서가 튜링 완전하지 않다는 사실을 알고 있기 때문에, 위의 것은 결코 로 확장되지 않습니다.A B만약 그렇다면GLUE확장될 것이다MACRO그리고.MACRO확장될 것이다GLUE그것은 무한 재귀의 가능성으로 이어질 것이며, 아마도 Cpp의 튜링 완전성을 암시할 것이다.따라서 현재 출시되어 있는 프리프로세서 마법사에 있어서는 위의 매크로가 확장되지 않는 것이 보증됩니다.

실패가 문제가 아니라 다음과 같은 문제가 있습니다.어디서요? 프리프로세서가 확장을 멈추기로 한 곳이 어디죠?

단계 분석:

  • 스텝 1은 매크로를 표시합니다.EXPAND및 인수 목록의 검색GLUE(MACRO, ())위해서X
  • 스텝 2는 인식한다.GLUE(MACRO, ())매크로로서:
    • 스텝 1(표준)은MACRO그리고.()의론으로서
    • 2단계는 스캔하지만 매크로를 찾을 수 없습니다.
    • 3단계는 매크로 본문에 삽입하여 다음 결과를 산출합니다.MACRO ()
    • 스텝 4는 억제한다.GLUE및 스캔MACRO ()매크로, 검색MACRO
      • 스텝 1(실행)은 인수의 빈 토큰시퀀스를 가져옵니다.
      • 스텝 2는 빈 시퀀스를 스캔하고 아무것도 하지 않습니다.
      • 3단계 매크로 본문에 삽입GLUE(A,B)
      • 스텝 4 스캔GLUE(A,B)매크로, 검색GLUE그러나 억제되어 있기 때문에 그대로 남습니다.
  • 그래서 최종적인 가치는X2단계 이후는GLUE(A,B)(의 4단계에 있지 않기 때문에GLUE(이론적으로는 더 이상 억제되지 않습니다)
  • 3단계는 그것을 몸에 삽입하여GLUE(A,B)
  • 스텝 4는 억제한다.EXPAND및 스캔GLUE(A,B)추가 매크로, 검색GLUE(으으)
    • 스텝 1은A그리고.B의론의 경우(oh no)
    • 스텝 2에서는 아무것도 할 수 없습니다.
    • 3단계 체내 대체품 제공A B(음...)
    • 스텝 4 스캔A B매크로를 검색해도 아무것도 찾을 수 없습니다.
  • 최종 결과는 이다A B

그게 우리의 꿈이겠지슬프게도, 매크로가 확장되어GLUE(A,B).

그래서 우리의 질문은, 왜?

매크로 확장은 실제로 발생하는 단계를 이해해야만 이해할 수 있는 복잡한 프로세스입니다.

  1. 때에 「」가 계속됩니다).(token의 다음 입니다.)분할됩니다().,하지 않습니다(따라서 매크로 확장은 발생하지 않습니다)., §)입력 스트림에 직접 존재해야 하며 다른 매크로에는 존재할 수 없습니다.)

  2. 매크로 본문에 이름이 표시되는 각 매크로 인수 앞에는 다음이 없습니다.# ★★★★★★★★★★★★★★★★★」##또는 그 다음에##매크로가 확장되도록 "예약"되어 있습니다.인수 내에 있는 모든 매크로가 매크로 본문으로 대체되기 전에 재귀적으로 확장됩니다.

  3. 매크로 인수 토큰 스트림은 매크로 본문으로 대체됩니다. 관련된 # ★★★★★★★★★★★★★★★★★」##스텝 1 의 원래의 파서 토큰에 근거해, 조작이 변경(스트링화 또는 페이스트화)되어 치환됩니다(스텝2 는 이러한 조작은 행해지지 않습니다).

  4. 결과 매크로 본문 토큰 스트림은 매크로를 확장하기 위해 다시 검색되지만 현재 확장 중인 매크로를 무시합니다.이 시점에서 (스텝 1에서 스캔하여 해석한 후에) 입력에 추가 토큰을 인식된 매크로의 일부로 포함할 수 있습니다.

중요한 것은 두 가지 다른 재귀 확장이 발생하며(위의 2단계와 4단계), 4단계 중 하나만 동일한 매크로의 재귀 매크로 확장을 무시한다는 것입니다.스텝 2의 재귀 확장에서는 현재 매크로가 무시되지 않으므로 재귀적으로 확장할 수 있습니다.

위의 예에서는 어떤 일이 일어나는지 살펴보겠습니다.입력의 경우

EXPAND(TEST PARENTHESIS())
  • 에서는 매크로가 됩니다.EXPAND list " 및 in argument list " 입니다.TEST PARENTHESIS()★★★★★★에X
  • 는 2단계는 2단계는 .TEST다음은 )(합니다.PARENTHESIS다음과 같습니다.
    • 스텝 1(실행)은 인수의 빈 토큰시퀀스를 가져옵니다.
    • 스텝 2는 빈 시퀀스를 스캔하고 아무것도 하지 않습니다.
    • 본문에 됩니다.()그 점을 양보하다()
    • 는 '4'를 스캔한다.() 및 finds finds and for for for for for 。
  • 인 '마지막 값'은X는 2입니다.TEST ()
  • 하여 3단계는 3단계입니다.TEST ()
  • 는 4를 억제합니다.EXPAND더 .TEST
    • 스텝 1은 인수의 빈 시퀀스를 가져옵니다.
    • 스텝 2는 아무것도 하지 않는다.
    • 하여 3단계 체내로 한다.EXPAND(0)
    • 되어 「4」를 합니다.TEST때 둘 EXPAND ★★★★★★★★★★★★★★★★★」TEST(4에 의해)도 일어나지 않습니다(스텝 4 확장

예시는 른른 your yourEXPAND(TEST())

  • 1 - 스텝 1EXPAND되고 있습니다.TEST()됩니다.X
  • 스텝 2, 이 스트림은 재귀적으로 해석됩니다.2이므로, 「」 「」 「2」입니다.EXPAND억제되어 있지 않다
    • 1 - 스텝 1TEST는 빈 됩니다.
    • 스텝 2 - nothing (빈 토큰시퀀스에 매크로 없음)
    • 하여 3단계, 3단계, 3단계, 3단계, 3단계EXPAND(0)
    • 4, 스텝 4,TEST억제되어 결과가 재귀적으로 확대됩니다.
      • 1, 스텝 1,EXPAND이 「해 주세요」만).TEST재귀에 됩니다.EXPAND되지 않습니다).0
      • 2, 스텝 2,0 일도 않습니다.
      • 3번, 3번, 3번, 3번, 3번, 3번0
      • 4, 스텝 4,0매크로를 다시 스캔합니다(또한 아무 일도 일어나지 않습니다).
  • 스텝 3,0인수로 대체됩니다.X최초의 몸으로EXPAND
  • 스텝 4,0매크로를 다시 스캔합니다(또한 아무 일도 일어나지 않습니다).

그래서 최종 결과는0

이 상황에서 매크로 치환에는 다음 3가지 단계가 있습니다.

  1. 인수에 대해 매크로 치환을 수행합니다.
  2. 매크로를 정의로 바꾸고 매개 변수를 인수로 바꿉니다.
  3. 교체된 매크로 이름을 사용하지 않고 결과를 다시 검색하여 추가 치환하십시오.

EXPAND(TEST PARENTHESIS()):

  • 스텝 1: 매크로 치환이 다음 인수에 대해EXPAND,TEST PARENTHESIS():
    • TEST괄호 뒤에 괄호가 붙어 있지 않기 때문에 매크로 호출로 해석되지 않습니다.
    • PARENTHESIS()는 매크로 기동이기 때문에, 다음의 3개의 순서가 실행됩니다.인수가 비어 있기 때문에 인수는 처리되지 않습니다.그리고나서PARENTHESIS()에 의해 대체됩니다.().그리고나서()는 재스캔 되어 매크로를 찾을 수 없습니다.
    • 스텝 1이 종료되고,EXPAND(TEST ()). (TEST ()매크로 치환의 결과가 아니기 때문에 재스캔되지 않습니다).
  • 순서 2EXPAND(TEST ())로 대체되었습니다.TEST ().
  • 순서 3TEST ()를 억제하는 동안 다시 검색됩니다.EXPAND:
    • 스텝 1 인수가 비어 있기 때문에 인수는 처리되지 않습니다.
    • 순서 2TEST ()에 의해 대체됩니다.EXPAND(0).
    • 순서 3EXPAND(0)재스캔 됩니다만,EXPAND억제되어 있습니다.

EXPAND(TEST ()):

  • 스텝 1: 매크로 치환이 다음 인수에 대해EXPAND:
    • 스텝 1의 인수TEST비어있기 때문에 처리가 이루어지지 않습니다.
    • 순서 2TEST ()에 의해 대체됩니다.EXPAND(0).
    • 순서 3에서는, 이 교환품을 재스캔 해,EXPAND(0)에 의해 대체됩니다.0.
  • 순서 2EXPAND(TEST ())되었다EXPAND(0),그리고.EXPAND(0)에 의해 대체됩니다.0.
  • 순서 30는 추가 매크로를 위해 재스캔되지만 없습니다.

질문의 다른 예도 이와 유사합니다.결론은 다음과 같습니다.

  • TEST PARENTHESIS(), 뒤에 괄호가 없습니다.TEST인수가 둘러싸인 매크로 호출로 처리되는 동안 확장되지 않습니다.
  • 그 뒤에 괄호를 붙입니다.PARENTHESIS확장되어 있지만, 이것은TEST는스캔되었습니다.인수처리중에는스캔되지않습니다.
  • 동봉된 매크로가 교체된 후TEST를 다시 검색하여 대체하지만, 이 시점에서는 둘러싸인 매크로의 이름이 표시되지 않습니다.

Chris Dodd의 훌륭한 답변을 읽고 잠시 생각을 한 후, 나는 그 문제에 대해 머리를 싸매고 있다고 생각한다.

C 프리프로세서를 정상인처럼 사용하고 있다면 여기서 피하는 것은 문제 없습니다.서로에 대해 언급하는 매크로를 만들지 마십시오. 그러면 괜찮을 것입니다.하지만 다크 아트를 만지작거리고 있다면, 당신은 놀랍게도 위의 문제를 발견하는 것이 쉽다는 것을 알게 될 것이다.자, 이제 이걸 피하는 방법을 설명하겠습니다.

여기서 왜 이런 일이 일어나는지에 대해서는 설명하지 않겠습니다(Chris의 답변은 그 이유를 이미 잘 설명하고 있습니다). 다만, 이러한 이 일어나는 장소와 해결 방법대해 설명하겠습니다.


매크로콜」의 확장이 연기 또는 간접적인 경우, 이 문제가 발생할 수 있습니다.즉, '간접'이란, 그 자리에서 바로 할 수 없는 확장이며, 일부 연결이나 대체/교체 후에만 가능합니다.이를 이해하기 위해 먼저 안전한 예를 살펴보겠습니다.

#define EXPAND(MACRO) MACRO
#define MY_MACRO(A, B) A##B

EXPAND(MY_MACRO(1,2)) // 12

여기서,MY_MACRO(1,2)는 매크로에 대한 직접적인 참조이며 그대로 확장해야 하는 적절한 인수를 가지고 있습니다.따라서 프리프로세서는 이 프로세서를 즉시 확장한 후,EXPAND.

이제 다음 예시와 비교해 보겠습니다.

#define EXPAND(MACRO, PAREN_ARGS) MACRO PAREN_ARGS
#define MY_MACRO(A, B) A##B

EXPAND(MY_MACRO, (1,2)) // 12
#define EXPAND(MACRO) MACRO (1,2)
#define MY_MACRO(A, B) A##B

EXPAND(MY_MACRO) // 12

내부 인수는EXPAND("MY_MACRO, (1,2)" 및 "MY_MACRO") "매크로처럼 보이지 않습니다."마지막으로 완전히 확장해야 하는데 인수 내에서 바로 확장되도록 형식이 올바르지 않습니다.현재는 정상적으로 동작하고 있습니다만, 상호 참조가 없기 때문에, 그 확장은 다음 달로 치환 후로 연기됩니다.EXPAND문제가 될 수 있습니다.

인수의 매크로를 그대로 확장할 수 없는 경우, 확장의 어느 시점에서도 「콜」을 포함할 없습니다.

위의 예에 상호 참조를 추가하여 이를 나타낼 수 있습니다.

#define EXPAND(MACRO, PAREN_ARGS) MACRO PAREN_ARGS
// Now MY_MACRO references EXPAND
#define MY_MACRO(A, B) EXPAND(A,B)
// Fails to expand
EXPAND(MY_MACRO, (1,2)) // EXPAND(1,2)
  ^       ^
  |       |
  |    This guy is postponed...
  |
...so it cannot eventually expand to this guy.
#define EXPAND(MACRO) MACRO (1,2)
// Now MY_MACRO references EXPAND
#define MY_MACRO(A, B) EXPAND(A)
// Fails to expand
EXPAND(MY_MACRO) // EXPAND(1)
  ^       ^
  |       |
  |    This guy is postponed...
  |
...so it cannot eventually expand to this guy.

한편 간접적인 확장이 없는 예에서는 완전히 안전하며 예상대로 평가됩니다.

#define EXPAND(MACRO) MACRO
// Now MY_MACRO references EXPAND
#define MY_MACRO(A, B) EXPAND(A)
// Succeeds
EXPAND(MY_MACRO(1,2)) // 1
  ^       ^
  |       |
  |    This guy can expand right here...
  |
...so it can freely expand to this guy.

부터MY_MACRO(1,2)는 실제 매크로이므로 즉시 평가할 수 있으며 문제가 발생하지 않습니다.

만약 제 판단이 맞다면, 여러분은 이전의 "calle"에 대해 걱정할 필요가 없습니다. 단지 "postened macro"를 논쟁으로 직접 가지고 있는 것에 불과합니다.다른 인수에 대해서도 걱정할 필요가 없습니다.프리프로세서는 치환 전에 인수의 완전한 확장을 시도합니다.게다가 매크로의 완전한 확장이 필요 없는 경우, 이것은 문제가 되지 않습니다.

단, 인수 내의 매크로가 완전히 삭제되고 그 확장이 간접적으로 이루어지는 경우에는 다시 한 번 확인하십시오.

언급URL : https://stackoverflow.com/questions/66593868/understanding-the-behavior-of-cs-preprocessor-when-a-macro-indirectly-expands-i

반응형