고급 전처리기에 대한 정보 및 트릭

본 문서에서는 몇 가지 고급 전처리기 주제에 대해 살펴보겠습니다. 먼저, 일반적인 위험을 피하는 것에 중점을 두어 함수형 매크로에 대해 깊이 알아보겠습니다. 여기서는 # 및 ## 전처리 연산자를 소개하고 매크로를 정의할 때 이러한 연산자를 사용하는 방법에 대해 설명합니다. "do { ... } while(0)" 트릭에 대해도 소개합니다. #if 또는 #ifdef가 조건부 컴파일에 더 적합한지 여부에 대한 논의를 끝으로 소개하겠습니다.

매크로 함수의 문제점

언뜻 보기에는 함수형 매크로가 단순하고 간단한 구성처럼 보입니다. 하지만, 그것들을 더 자세히 조사하면 여러분은 매우 짜증나는 위협을 발견할 것입니다. 저는 각각의 문제가 분명하게 보이는 여러 가지 사례를 제시하고 이러한 문제를 해결할 수 있는 방법을 제안할 것입니다.

정의문의 매개 변수 주위에 항상 괄호를 표시합니다.

다음의 간단한 매크로를 확인합니다:

#define TIMES_TWO(x) x * 2

사소한 경우에는 이렇게 하면 작업이 완료됩니다. 예를 들어 TIMES_TWO(4)가 4 * 2로 확장되고, 이 값은 8로 평가됩니다. 반면 TIMES_TWO(4 + 5)는 18으로 평가될 것으로 예상되지만, 매크로 단순이 쓰여진 "x"를 매개 변수로 대체하기 때문에 그렇지 않습니다. 즉, 컴파일러가 "4 + 5 * 2"를 인식하며 이는 14로 평가됩니다.

해결 방법은 다음과 같이 매개 변수를 항상 괄호 안에 포함하는 것입니다.

#define TIMES_TWO(x) (x) * 2

매크로(식)는 괄호로 묶어야 합니다.

다음과 같은 매크로가 있다고 가정합니다:

#define PLUS1(x) (x) + 1

여기서는 변수 x 주위에 괄호를 올바르게 배치했습니다. 이 매크로는 일부 위치에서 작동합니다. 예를 들어 다음과 같이 11을 출력합니다.

printf("%d\n", PLUS1(10));

 

그러나 다른 상황에서는 결과가 다릅니다. 다음은 22가 아닌 21을 인쇄한 것입니다.

printf("%d\n", 2 * PLUS1(10));

 

이는 전처리기사가 단순히 매크로 호출을 소스 코드로 대체하기 때문입니다. 컴파일러의 시각은 다음과 같습니다.

printf("%d\n", 2 * (10) + 1);

분명히, 추가 전에 곱셈을 평가했기 때문에 이것이 우리가 원했던 것이 아닙니다.

해결책은 매크로의 전체 정의를 괄호 안에 넣는 것입니다.

#define PLUS1(x) ((x) + 1)

 

전처리는 이를 다음과 같이 확장하고 결과 "22"는 예상대로 인쇄됩니다.

printf("%d\n", 2 * ((10) + 1));

부작용이 있는 매크로 및 매개 변수

다음 매크로와 매크로를 사용하는 일반적인 방법을 고려하시기 바랍니다:

#define SQUARE(x) ((x) * (x))
printf("%d\n", SQUARE(++i));

 

매크로 사용자는 한 단계씩 증가하여 증가 후 제곱 값을 인쇄하려고 했을 수 있습니다. 대신 다음과 같이 확장됩니다.

printf("%d\n", ((++i) * (++i)));

문제는 매개 변수를 사용할 때마다 부작용이 발생한다는 것입니다. (예외로, 식에 "i"에 대한 두 가지 수정 사항이 포함되어 있기 때문에 결과 식은 C조차 제대로 정의되지 않습니다.)

여기서는 가능하면 각 매개 변수를 한 번만 평가해야 한다는 것이 원칙입니다. 이것이 불가능한 경우 잠재 사용자가 놀라지 않도록 문서화해야 합니다.

각 매개 변수를 정확히 한 번만 사용하는 매크로를 작성할 수 없다면 어떻게 해야 할까요? 이 질문에 대한 간단한 대답은 매크로 사용을 피하는 것입니다. 오늘날 인라인 기능은 모든 C++ 및 대부분의 C 컴파일러에서 지원되므로 매크로의 매개 변수 부작용 문제 없이 작업을 똑같이 잘 수행할 수 있습니다.또한 매크로와 같이 타입이 없는 것이 아니기 때문에 함수를 사용할 때 컴파일러가 오류를 보고하기가 더 쉽습니다.

특수 매크로 특징

"#" 연산자를 사용한 문자열 생성

"#" 연산자는 함수형 매크로에서 매개 변수를 문자열로 변환하는 데 사용할 수 있습니다. 처음에 이것은 매우 간단해 보이지만, 만약 여러분이 단순히 순진한 접근 방식을 매크로에서 바로 사용한다면, 안타깝게도, 여러분은 놀랄 것입니다.

예시:

#define NAIVE_STR(x) #x
puts(NAIVE_STR(10)); /* This will print "10". */

 

예상대로 작동하지 않는 예는 다음과 같습니다.

#define NAME Anders
printf("%s", NAIVE_STR(NAME)); /* Will print NAME. */

이 후자의 예에서는 NAME이 정의된 대상이 아닌 NAME을 인쇄합니다. 이는 의도한 것이 아닙니다. 다행히 이를 위한 표준 해결방법이 있습니다.

#define STR_HELPER(x) #x
#define STR(x) STR_HELPER(x)

이 기이한 구성의 배경에는 STR(NAME)이 확장되면 STR_HELPER(NAME)로 확장되고, 확장되기 전에 NAME과 같은 모든 개체와 같은 매크로가 먼저 대체된다는 것이 있습니다(교체할 매크로가 있는 한). 함수 같은 매크로 STR_HELPER가 호출되면 전달되는 매개 변수는 Anders입니다.

## 연산자를 사용하여 식별자를 연결

"##" 연산자는 작은 조각들을 더 큰 식별자, 숫자 등으로 결합해야 함을 나타내기 위해 전처리 매크로의 정의에 사용됩니다.

예를 들어 다음과 같은 변수 모음을 사용한다고 가정합니다.

MinTime, MaxTime, TimeCount.
MinSpeed, MaxSpeed, SpeedCount.

매크로, AVERAGE를 하나의 매개 변수로 정의하여 평균 시간, 속도 등을 반환할 수 있습니다. 가벼운 첫 접근 방식은 다음과 같습니다:

#define NAIVE_AVERAGE(x)
(((Max##x) - (Min##x)) / (x##Count))

 

이는 일반적인 사용 사례에 적용됩니다.

NAIVE_AVERAGE(Time);

이 경우 다음과 같이 확장됩니다.

return (((MaxTime) - (MinTime)) / (TimeCount));

그러나 위의 "#" 연산자와 마찬가지로 다음 컨텍스트에서 사용할 경우 의도한 대로 작동하지 않습니다:

#define TIME Time
NAIVE_AVERAGE(TIME)

 

안타깝게도 다음과 같이 확장됩니다.

return (((MaxTIME) - (MinTIME)) / (TIMECount));

 

위의 STR 사례에서처럼 이 문제를 해결하는 것이 쉽습니다. 매크로를 두 단계로 확장해야 합니다. 일반적으로 모든 항목을 결합하는 일반 매크로를 정의할 수 있습니다.

#define GLUE_HELPER(x, y) x##y
#define GLUE(x, y) GLUE_HELPER(x, y)

이제 AVERAGE 매크로에서 이 기능을 사용할 준비가 되었습니다.

#define AVERAGE(x)
(((GLUE(Max,x)) - (GLUE(Min,x))) / (GLUE(x,Count)))

"do {} while(0)" 트릭, 매크로를 문장처럼

매크로가 일반 C 코드와 동일한 모양과 느낌을 갖도록 매크로를 구현하는 것이 매우 편리합니다.

첫 번째 단계는 간단합니다. 상수에만 개체와 같은 매크로를 사용하십시오. 고유한 문에 배치될 수 있는 항목이나 시간에 따라 달라지는 표현식에 함수형 매크로를 사용합니다.

모양과 느낌을 유지함으로써 사용자가 기능 같은 매크로 뒤에 세미콜론을 쓸 수 있도록 하고 싶습니다. 전처리는 매크로를 소스 코드 조각으로만 대체하므로, 다음 세미콜론으로 인해 프로그램이 놀라지 않도록 해야 합니다.

예제:

void test()
{
 a_function(); /* The semicolon is not part of
 A_MACRO(); the macro substitution. */
}

 

하나의 문장으로 구성된 매크로의 경우 이는 간단합니다. 후행 세미콜론 없이 매크로를 정의하기만 하면 됩니다.

#define DO_ONE() a_function(1,2,3)

 

그러나 매크로에 두 개의 문(예: 두 개의 함수 호출)이 포함되어 있으면 어떻게 되는지, 왜 안좋은지 알아보겠습니다.

#define DO_TWO() first_function(); second_function()

 

간단한 맥락에서 이는 의도한 대로 작동합니다. 예를 들어 다음과 같습니다.

DO_TWO();

다음과 같이 확장됩니다:

first_function(); second_function();

 

그러나 단일 문장이 예상되는 상황에서 다음과 같은 상황이 발생할 수 있습니다.

if (... test something ...)
DO_TWO();

 

불행하게도 이는 다음과 같이 확장됩니다.

if (... test something ...)
 first_function(); 
second_function();

문제는 "first_function"만 "if" 문의 본문이 된다는 것입니다. "second_function"은 문의 일부가 아니므로 항상 호출됩니다.

그러면, 두 호출을 중괄호 안에 포함시키면, 사용자가 제공하는 세미콜론은 빈 문장이 되는 것인지 알아봅니다.

#define DO_TWO() \
{ first_function(); second_function(); }

유감스럽게도, 컨텍스트를 "만약"의 "다른" 조항으로 확장하더라도 이는 여전히 의도한 대로 작동하지 않습니다.

다음 예를 고려하십시오.

if (... test something ...)
 DO_TWO();
 else
 ...

 

이렇게 하면 다음과 같이 확장됩니다. 끝 괄호 뒤에 세미콜론을 붙입니다.

if (... test something ...)
 { first_function(); second_function(); };
else
 ...

if의 본문은 두 개의 문(괄호 안의 복합 문과 세미콜론으로 구성된 빈 문)으로 구성됩니다. 이는 법적 C가 아니므로 "만약(...)"과 "기타" 사이에 정확히 하나의 문장이 있어야 합니다!

#define DO_TWO() \
do { first_function(); second_function(); } while(0)

어디서 이걸 처음 봤는지는 기억이 안 나지만, 제가 얼마나 당황했었는지 그 설계의 훌륭함을 깨닫기 전에 기억해요. 이 트릭은 끝에 세미콜론이 있어야 하는 복합 문을 사용하는 것입니다. C에는 이러한 구문, 즉 "do ... while(...);"이 있습니다.
잠깐만요, 이게 루프라고 생각하실지도 몰라요! 한 번쯤은 해보고 싶어요. 그냥 넘어가지 말고요!
이번 사건에선 운이 좋았던 것뿐이에요 "do ... while(...);" 루프는 한 번 이상 루프된 다음 테스트 식이 참인 동안 계속됩니다. 자, 식이 절대 참이 되지 않도록 하고, 사소한 식 "0"은 항상 거짓이며, 루프 본문은 정확히 한 번 실행됩니다.
이 버전의 매크로에는 정상 기능의 모양과 느낌이 있습니다. "if ... else" 절에서 사용할 경우 매크로가 다음과 같은 올바른 C 코드로 확장됩니다.

if (... test something ...)
 do { first_function(); second_function(); } while(0); 
else
 ...

결론적으로 "do ... while(0)" 트릭은 일반 기능과 모양과 느낌이 동일한 기능 같은 매크로를 만들 때 유용합니다. 단점은 매크로 정의가 직관적이지 않아 보인다는 것입니다. 따라서 코드를 읽는 다른 사람이 저처럼 당황하지 않도록 "do"과 "while"의 목적에 대해 언급하는 것이 좋습니다.

이 방법을 사용하지 않기로 결정하더라도, 다음 번에 이 양식의 매크로를 발견할 때 이 방법을 인식할 수 있기를 바랍니다.

왜 #ifdefs보다 #ifs를 선호해야 하는지

대부분의 응용 프로그램에는 실제 소스 코드의 일부가 제외되는 일종의 구성이 필요합니다. 예를 들어 대체 구현으로 라이브러리를 작성하거나, 특정 운영 체제 또는 프로세서를 필요로 하는 코드를 애플리케이션에 포함하거나, 내부 테스트 중에 사용할 추적 출력을 애플리케이션에 포함할 수 있습니다.

앞서 설명한 것처럼 #if 및 #ifdef를 모두 사용하여 소스 코드의 일부를 컴파일하지 못하도록 제외할 수 있습니다.

#ifdefs

#ifdef를 사용하는 경우 코드는 다음과 같습니다.

#ifdef MY_COOL_FEATURE
... included if "my cool feature" is used ...
#endif
#ifndef MY_COOL_FEATURE
... excluded if "my cool feature" is used ... 
#endif

일반적으로 #ifdef를 사용하는 응용 프로그램에는 특별한 설정 변수 처리가 필요하지 않습니다.

#ifs

#ifs를 사용하는 경우 일반적으로 사용되는 사전 프로세서 기호가 항상 정의됩니다. #ifdef가 사용하는 기호에 해당하는 기호는 각각 정수 1과 0으로 나타낼 수 있는 참 또는 거짓입니다.

#if MY_COOL_FEATURE
 ... included if "my cool feature" is used ...
#endif
#if !MY_COOL_FEATURE
 ... excluded if "my cool feature" is used ... 
#endif

 

물론, 전처리기 기호는 다음과 같은 더 많은 상태를 가질 수 있습니다.

#if INTERFACE_VERSION == 0 
 printf("Hello\n"); 
#elif INTERFACE_VERSION == 1 
 print_in_color("Hello\n", RED);
#elif INTERFACE_VERSION == 2
 open_box("Hello\n");
#else 
#error "Unknown INTERFACE_VERSION" 
#endif

 

일반적으로 이 스타일을 사용하는 응용 프로그램은 모든 구성 변수의 기본값을 강제로 지정해야 합니다. 예를 들어 "defaults.h"와 같은 파일에서 이 작업을 수행할 수 있습니다. 응용 프로그램을 구성할 때 명령줄에 일부 기호를 지정하거나 특정 구성 헤더 파일에 "config.h"라고 말할 수 있습니다. 기본 구성을 사용해야 하는 경우 이 구성 헤더 파일을 비워 둘 수 있습니다.

defaults.h 헤더 파일의 예시입니다:

/* defaults.h for the application. */
#include "config.h"
/*
* MY_COOL_FEATURE -- True, if my cool feature 
* should be used. 
 */
#ifndef MY_COOL_FEATURE
#define

#if vs #ifdef 어떤 것을 사용해야 하는가

지금까지는 두 방법이 상당히 같아 보입니다. 실제 애플리케이션을 살펴보면 두 가지 애플리케이션이 모두 공통적으로 사용된다는 것을 알 수 있습니다. 언뜻 보기에 #iffdefs는 다루기 쉬워 보이지만, 경험을 통해 장기적으로 #ifs가 더 우수하다는 것을 알게 되었습니다.

#ifdefs 가 철자가 틀린 단어로부터 당신을 보호하지 않는다. #ifs 또한 마찬가지

#ifdef는 식별자의 철자가 잘못되었는지 여부를 알 수 없습니다. 특정 식별자가 정의되어 있는지 여부만 알 수 있기 때문입니다.

예를 들어 다음 오류는 컴파일을 통해 인식되지 않습니다.

#ifdef MY_COOL_FUTURE /* Should be "FEATURE". */
 ... Do something important ... 
#endif

 

반면에 대부분의 컴파일러는 정의되지 않은 기호가 #if 지시어에 사용되었음을 감지할 수 있습니다. C 표준은 이것이 가능해야 하며, 이 경우 기호는 값 0을 가져야 한다고 말합니다. (IAR Systems 컴파일러의 경우 진단 메시지 Pe193이 발행됩니다. 기본적으로 이 내용은 경고이거나 오류일 수 있습니다.)

#ifdefs are not future safe

프로그램이 #ifdefs를 사용하여 구성된다고 생각해 보겠습니다. 나중에 기본값이 변경되더라도 속성을 특정 방식으로 구성하려는 경우(예: 색상을 지원해야 하는 경우)에는 어떻게 됩니까? 불행하게도 이렇게는 할 수 업습니다

반면에 #ifs를 사용하여 응용 프로그램을 구성하는 경우 기본값이 변경되는 경우 미래 안전을 보장하기 위해 구성 변수를 특정 값으로 설정할 수 있습니다.

For #ifdefs, the default value dictates the name

응용 프로그램을 구성하는 데 #iffdef가 사용되는 경우 기본 구성은 추가 기호를 지정하지 않는 것입니다. 추가 기능의 경우, 간단히 MY_COOL_FEATURE를 정의하면 됩니다. 그러나 식별자의 이름이 종종 DONT_USE_COLORS가 됩니다.

이중 부정은 강한 긍정인 것인가?

한 가지 단점은 이중 부정이 발생하기 때문에 코드를 읽고 쓰기가 더 어려워진다는 것입니다. 예를 들어 색상 지원을 위해 다음과 같은 일부 코드가 포함되어야 합니다.

#ifndef DONT_USE_COLORS
... do something ... 
#endif

 

세부 사항처럼 들릴 수도 있지만, 코드의 많은 부분을 찾아본다면 머지않아 혼란스러워질 것입니다. 최소한 저는 그렇게 생각합니다. 다음을 정말 선호합니다.

#if USE_COLORS
... do something ... 
#endif

코드 작성 시 기본값을 알고 있어야 합니다.
또 다른 단점은 프로그램을 작성할 때 기능이 기본적으로 설정되어 있는지 여부를 알고 있어야 한다는 것입니다. 철자가 틀린 단어에 대한 보호 수단이 없다는 사실과 함께, 이것은 사고가 일어나기를 기다리는 것입니다.

#ifdefs의 기본 값을 변경할 수 없습니다.

그러나 가장 큰 단점은 #ifdefs를 사용할 때 프로그램 전체에서 모든 #ifdef를 변경하지 않고는 기본값을 변경할 수 없다는 점입니다.

#ifs의 경우 기본값을 변경하는 것은 사소한 일입니다. 기본값을 포함하는 파일만 업데이트하면 됩니다.

#ifdefs 대신 #ifs 사용

모든 것이 괜찮아 보이고, 현재 프로그램에 #ifdefs를 사용하고 있기에 그대로 사용해야 할 것 같습니다.

하지만 그럴 필요 없습니다! #ifdefs 대신 #ifs를 사용하는 것은 사소한 일입니다. 또한 이전 설정한 변수에 대해 이전 버전과의 호환성을 제공합니다.

먼저 새 구성 변수의 이름을 지정해야 합니다. 부정형 이름(예: DONT_USE_COLORS)의 변수 이름은 긍정형 형식(예: USE_COLORS)으로 변경해야 합니다. 긍정형 이름을 가진 변수는 이름을 유지하거나 이름을 약간 변경할 수 있습니다.

설정 변수의 이름을 유지하고 사용자가 해당 변수를 비어 있는 것으로 정의한 경우(""#define MY_COOL_FEATURE" 에서와 같이) 해당 기호를 사용하는 경우 첫 번째 #에서 컴파일 오류가 발생합니다. 대신 사용자가 정의 항목을 1로 지정하기만 하면 됩니다.

defaults.h 헤더 파일을 생성합니다, 위에서 설명한 대로 모든 소스 파일에 이 파일이 포함되어 있는지 확인합니다. (이 파일이 포함되지 않으면 구성 변수를 사용하는 즉시 정의되지 않으므로 오류가 발생합니다.) 헤더 파일의 시작 부분에서 이전 #ifdef 이름을 새 이름에 매핑할 수 있습니다. 예를 들면 다음과 같습니다.

/* Old configuration variables, ensure that they still work. */
#ifdef DONT_USE_COLORS 
#define USE_COLORS 0 
#endif
/* Set the default. */ 
#ifndef USE_COLORS 
#define USE_COLORS 1 
#endif

그런 다음 모든 소스 파일에서 #ifdefs의 모든 항목 이름을 #ifs로 변경합니다.

From: To:
#ifdef MY_COOL_FEATURE #if MY_COOL_FEATURE 
#ifndef MY_COOL_FEATURE  #if !MY_COOL_FEATURE
   
#ifdef DONT_USE_COLORS #if !USE_COLORS 
#ifndef DONT_USE_COLORS #if USE_COLORS

최종 결과는 현재 기본 설정과 함께 모든 구성 변수가 하나의 중앙 위치에서 정의되는 프로그램입니다. 항상 쓰려고 계획했지만 전혀 활용하지 못한 의견을 저장할 수 있는 완벽한 장소입니다.

죄송하지만, 당사 사이트에서는 Internet Explorer를 지원하지 않습니다.보다 편안한 사이트를 위해 Chrome, Edge, Firefox 등과 같은 최신 브라우저를 사용해 주시길 부탁드립니다.