전 처리기 매크로가 왜 사악하고 대안은 무엇입니까?


94

나는 항상 이것을 물었지만 정말 좋은 대답을받지 못했습니다. 나는 첫 번째 "Hello World"를 작성하기 전에 거의 모든 프로그래머가 "매크로는 절대 사용해서는 안된다", "매크로는 악하다"와 같은 문구를 접했다고 생각합니다. 내 질문은 다음과 같습니다. 왜? 새로운 C ++ 11을 사용하면 수년이 지난 후에 진정한 대안이 있습니까?

쉬운 부분은 #pragma플랫폼 #pragma once에 따라 다르고 컴파일러 에 따라 달라지는 매크로에 관한 것입니다 . 대부분의 경우 이러한 심각한 결점 은 적어도 두 가지 중요한 상황에서 오류가 발생하기 쉽습니다. 다른 경로에 같은 이름과 일부 네트워크 설정 및 파일 시스템이 있습니다.

그러나 일반적으로 매크로와 그 사용에 대한 대안은 어떻습니까?


19
#pragma매크로가 아닙니다.
FooF

1
@foof 전 처리기 지시문?
user1849534 dec.

6
@ user1849534 : 예, 그게 바로 그거 고 ... 매크로에 관한 조언은 #pragma.
Ben Voigt 2012

1
당신과 많이 할 수있는 constexpr, inline기능을하고 templates있지만 boost.preprocessorchaos매크로가 자신의 자리를 가지고 있음을 보여준다. 차이 컴파일러, 플랫폼 등의 구성 매크로를 언급합니다
브랜든

답변:


165

매크로는 다른 도구와 같습니다. 살인에 사용되는 망치는 망치이기 때문에 악의가 없습니다. 그 사람이 그런 식으로 사용하는 방식은 악합니다. 못으로 망치고 싶다면 망치가 완벽한 도구입니다.

매크로를 "나쁜"것으로 만드는 몇 가지 측면이 있습니다 (나중에 각각에 대해 자세히 설명하고 대안을 제안 할 것입니다).

  1. 매크로를 디버그 할 수 없습니다.
  2. 매크로 확장은 이상한 부작용을 일으킬 수 있습니다.
  3. 매크로에는 "네임 스페이스"가 없으므로 다른 곳에서 사용 된 이름과 충돌하는 매크로가있는 경우 원하지 않는 위치에서 매크로가 대체되고 일반적으로 이상한 오류 메시지가 표시됩니다.
  4. 매크로는 인식하지 못하는 것에 영향을 미칠 수 있습니다.

여기에서 조금 확장 해 보겠습니다.

1) 매크로는 디버깅 할 수 없습니다. 숫자 나 문자열로 변환되는 매크로가있는 경우 소스 코드에는 매크로 이름이 있고 많은 디버거가 있으면 매크로가 무엇으로 변환되는지 "볼"수 없습니다. 그래서 당신은 실제로 무슨 일이 일어나고 있는지 알지 못합니다.

교체 : 사용 enum또는const T

"함수와 유사한"매크로의 경우 디버거가 "현재있는 소스 행당"수준에서 작동하기 때문에 매크로가 하나의 명령문이든 100 개이든 상관없이 단일 명령문처럼 작동합니다. 무슨 일이 일어나고 있는지 파악하기 어렵게 만듭니다.

대체 : 함수 사용- "빠름"이 필요한 경우 인라인 (하지만 너무 많은 인라인은 좋지 않음)

2) 매크로 확장은 이상한 부작용을 일으킬 수 있습니다.

유명한 것은 #define SQUARE(x) ((x) * (x))그리고 사용 x2 = SQUARE(x++)입니다. 이로 x2 = (x++) * (x++);인해 유효한 코드 [1]이더라도 프로그래머가 원하는 바가 거의 확실하지 않습니다. 함수라면 x ++를해도 괜찮을 것이고 x는 한 번만 증가 할 것입니다.

또 다른 예는 매크로의 "if else"입니다.

#define safe_divide(res, x, y)   if (y != 0) res = x/y;

그리고

if (something) safe_divide(b, a, x);
else printf("Something is not set...");

실제로 완전히 잘못된 것이됩니다 ....

교체 : 실제 기능.

3) 매크로에는 네임 스페이스가 없습니다.

매크로가있는 경우 :

#define begin() x = 0

그리고 begin을 사용하는 C ++ 코드가 있습니다.

std::vector<int> v;

... stuff is loaded into v ... 

for (std::vector<int>::iterator it = myvector.begin() ; it != myvector.end(); ++it)
   std::cout << ' ' << *it;

이제 어떤 오류 메시지가 발생한다고 생각하고 오류를 어디에서 찾습니까? [다른 사람이 작성한 일부 헤더 파일에있는 시작 매크로를 완전히 잊었거나 알지 못했다고 가정 할 때]? [포함하기 전에 해당 매크로를 포함하면 훨씬 더 재미 있습니다. 코드 자체를 볼 때 전혀 의미가없는 이상한 오류에 빠져들 것입니다.

대체 : "규칙"만큼 대체는 없습니다. 매크로에는 대문자 이름 만 사용하고 다른 항목에는 모두 대문자 이름을 사용하지 마십시오.

4) 매크로에는 사용자가 알지 못하는 효과가 있습니다.

이 기능을 사용하십시오.

#define begin() x = 0
#define end() x = 17
... a few thousand lines of stuff here ... 
void dostuff()
{
    int x = 7;

    begin();

    ... more code using x ... 

    printf("x=%d\n", x);

    end();

}

이제 매크로를 보지 않고도 begin이 x에 영향을 미치지 않는 함수라고 생각할 것입니다.

이런 종류의 것, 그리고 훨씬 더 복잡한 예제를 보았습니다. 정말 당신의 하루를 망칠 수 있습니다!

대체 : 매크로를 사용하여 x를 설정하거나 x를 인수로 전달하십시오.

매크로를 사용하는 것이 확실히 유익 할 때가 있습니다. 한 가지 예는 파일 / 라인 정보를 전달할 매크로로 함수를 래핑하는 것입니다.

#define malloc(x) my_debug_malloc(x, __FILE__, __LINE__)
#define free(x)  my_debug_free(x, __FILE__, __LINE__)

이제 my_debug_malloc코드에서 일반 malloc으로 사용할 수 있지만 추가 인수가 있으므로 마지막에 "어떤 메모리 요소가 해제되지 않았는지"스캔하면 할당 된 위치를 인쇄 할 수 있습니다. 프로그래머는 누수를 추적 할 수 있습니다.

[1] "시퀀스 포인트에서"하나의 변수를 두 번 이상 업데이트하는 것은 정의되지 않은 동작입니다. 시퀀스 포인트는 문장과 정확히 같지는 않지만 대부분의 의도와 목적에 대해 우리가 그것을 고려해야하는 것입니다. 이렇게 x++ * x++하면 x두 번 업데이트되며 , 이는 정의되지 않았으며 아마도 다른 시스템에서 다른 값으로 이어질 것이고 다른 결과 값으로 이어질 것입니다 x.


6
if else문제 매크로 본체 내부에 배치함으로써 해결 될 수있다 do { ... } while(0). 하나이 동작합니다은에 대한 기대 iffor기타 잠재적으로 위험 제어 흐름 문제와. 그러나 예, 실제 기능이 일반적으로 더 나은 솔루션입니다. #define macro(arg1) do { int x = func(arg1); func2(x0); } while(0)
아론 McDaid

11
@AaronMcDaid : 예, 이러한 매크로에 노출 된 일부 문제를 해결하는 몇 가지 해결 방법이 있습니다. 내 게시물의 요점은 매크로를 잘 수행하는 방법을 보여주는 것이 아니라 좋은 대안이있는 "매크로를 잘못 만드는 것이 얼마나 쉬운 지"였습니다. 즉, 매크로가 매우 쉽게 해결하는 것이 있으며 매크로가 올바른 작업을 수행 할 때도 있습니다.
Mats Petersson 2013 년

1
요점 3에서 오류는 더 이상 실제로 문제가되지 않습니다. Clang과 같은 최신 컴파일러는 다음과 같이 말하고 정의 된 note: expanded from macro 'begin'위치를 표시 begin합니다.
kirbyfan64sos

5
매크로는 다른 언어로 번역하기 어렵습니다.
Marco van de Voort

1
@FrancescoDondi : stackoverflow.com/questions/4176328/... . (그 대답 상당히 아래로, 그것은 내가 대해 이야기 ++ * 내가 ++와 같은
매트 피터슨

21

"매크로는 악하다"라는 말은 일반적으로 #pragma가 아닌 #define을 사용하는 것을 의미합니다.

구체적으로 표현은 다음 두 가지 경우를 나타냅니다.

  • 매직 넘버를 매크로로 정의

  • 매크로를 사용하여 표현식 바꾸기

새로운 C ++ 11을 사용하면 수년이 지난 후에 진정한 대안이 있습니까?

예, 위 목록에있는 항목의 경우 (매직 넘버는 const / constexpr로 정의해야하며 표현식은 [normal / inline / template / inline template] 함수로 정의해야합니다.

다음은 매직 넘버를 매크로로 정의하고 표현식을 매크로로 대체하여 발생하는 몇 가지 문제입니다 (해당 표현식을 평가하기위한 함수를 정의하는 대신).

  • 매직 넘버에 대한 매크로를 정의 할 때 컴파일러는 정의 된 값에 대한 유형 정보를 유지하지 않습니다. 이로 인해 컴파일 경고 (및 오류)가 발생하고 사람들이 코드를 디버깅하는 데 혼란을 줄 수 있습니다.

  • 함수 대신 매크로를 정의 할 때 해당 코드를 사용하는 프로그래머는 매크로가 함수처럼 작동하기를 기대하지만 그렇지 않습니다.

이 코드를 고려하십시오.

#define max(a, b) ( ((a) > (b)) ? (a) : (b) )

int a = 5;
int b = 4;

int c = max(++a, b);

c에 할당 한 후 a와 c가 6이 될 것으로 예상합니다 (매크로 대신 std :: max를 사용하는 경우). 대신 코드는 다음을 수행합니다.

int c = ( ((++a) ? (b)) ? (++a) : (b) ); // after this, c = a = 7

또한 매크로는 네임 스페이스를 지원하지 않으므로 코드에서 매크로를 정의하면 사용할 수있는 이름으로 클라이언트 코드가 제한됩니다.

즉, 위의 매크로 (max)를 정의 #include <algorithm>하면 명시 적으로 작성하지 않는 한 더 이상 아래 코드 를 사용할 수 없습니다 .

#ifdef max
#undef max
#endif
#include <algorithm>

변수 / 함수 대신 매크로를 사용한다는 것은 해당 주소를 사용할 수 없음을 의미합니다.

  • 매크로 상수가 매직 넘버로 평가되면 주소로 전달할 수 없습니다.

  • 함수로서의 매크로의 경우 술어로 사용하거나 함수의 주소를 취하거나 펑터로 취급 할 수 없습니다.

편집 : 예를 들어, 위의 올바른 대안 #define max:

template<typename T>
inline T max(const T& a, const T& b)
{
    return a > b ? a : b;
}

이는 매크로가 수행하는 모든 작업을 수행하지만 한 가지 제한 사항이 있습니다. 인수 유형이 다른 경우 템플릿 버전은 사용자를 명시 적으로 지정해야합니다 (실제로 더 안전하고 명시적인 코드로 이어짐).

int a = 0;
double b = 1.;
max(a, b);

이 최대 값이 매크로로 정의되면 코드가 컴파일됩니다 (경고와 함께).

이 최대 값이 템플릿 함수로 정의 된 경우 컴파일러는 모호함을 지적하고 max<int>(a, b)또는 중 하나를 말해야합니다 max<double>(a, b)(따라서 의도를 명시 적으로 명시).


1
C ++ 11에 국한 될 필요는 없습니다. 단순히 함수를 사용하여 매크로를 표현식으로 바꾸고 [정적] const / constexpr을 사용하여 매크로를 상수로 바꾸면됩니다.
utnapistim

1
C99에서도를 사용할 const int someconstant = 437;수 있으며 매크로가 사용되는 거의 모든 방식으로 사용할 수 있습니다. 작은 기능에도 마찬가지입니다. C의 정규식에서 작동하지 않는 매크로로 무언가를 작성할 수있는 몇 가지가 있습니다 (C에서 할 수없는 모든 유형의 숫자 ​​배열을 평균화하는 무언가를 만들 수 있지만 C ++에는 템플릿이 있습니다) 그에 대한). C ++ 11은 "이를 위해 매크로가 필요하지 않다"는 몇 가지 사항을 더 추가하지만 대부분 이전 C / C ++에서 이미 해결되었습니다.
Mats Petersson

인수를 전달하면서 사전 증가를 수행하는 것은 끔찍한 코딩 관행입니다. 그리고 C / C ++로 코딩하는 사람 은 함수와 같은 호출이 매크로가 아니라고 가정 해서는 안됩니다 .
StephenG

많은 구현은 자발적으로 식별자를 괄호로 max하고 min그들은 왼쪽}하는 경우. 하지만 그러한 매크로를 정의해서는 안됩니다 ...
LF

14

일반적인 문제는 다음과 같습니다.

#define DIV(a,b) a / b

printf("25 / (3+2) = %d", DIV(25,3+2));

전처리 기가 다음과 같이 확장하기 때문에 5가 아닌 10을 인쇄합니다.

printf("25 / (3+2) = %d", 25 / 3 + 2);

이 버전이 더 안전합니다.

#define DIV(a,b) (a) / (b)

2
흥미로운 예는 기본적으로 의미가없는 토큰
일뿐입니다.

예. 매크로에 제공되는 방식으로 확장됩니다. 그만큼DIV매크로 () 주위의 한 쌍의 재 기입 될 수있다 b.
phaazon

2
당신은 의미 #define DIV(a,b)하지#define DIV (a,b) 은 매우 다른 .
rici

6
#define DIV(a,b) (a) / (b)충분하지 않습니다. 일반적으로 다음과 같이 항상 가장 바깥 쪽 대괄호를 추가합니다.#define DIV(a,b) ( (a) / (b) )
PJTraill

3

매크로는 특히 일반적인 코드 (매크로의 매개 변수는 무엇이든 될 수 있음)를 만드는 데 유용하며 때로는 매개 변수를 포함합니다.

또한이 코드는 매크로가 사용되는 지점에 배치 (삽입)됩니다.

OTOH, 비슷한 결과를 얻을 수 있습니다 :

  • 오버로드 된 함수 (다른 매개 변수 유형)

  • 템플릿, C ++ (일반 매개 변수 유형 및 값)

  • 인라인 함수 (단일 포인트 정의로 점프하는 대신 호출되는 곳에 코드를 배치합니다. 그러나 이는 컴파일러에 대한 권장 사항입니다).

편집하다 : 왜 매크로가 나쁜지 :

1) 인수의 유형 검사가 없으므로 (유형이 없음) 쉽게 오용 될 수 있습니다. 2) 때로는 매우 복잡한 코드로 확장되어 전처리 된 파일에서 식별하고 이해하기 어려울 수 있습니다. 3) 오류를 만들기 쉽습니다. -다음과 같은 매크로에서 발생하기 쉬운 코드 :

#define MULTIPLY(a,b) a*b

그리고 전화

MULTIPLY(2+3,4+5)

확장되는

2 + 3 * 4 + 5 (그리고 안으로 들어 가지 않음 : (2 + 3) * (4 + 5)).

후자를 사용하려면 다음을 정의해야합니다.

#define MULTIPLY(a,b) ((a)*(b))

3

전 처리기 정의 나 매크로를 호출 할 때 사용하는 데 문제가 있다고 생각하지 않습니다.

그것들은 c / c ++에서 발견되는 (메타) 언어 개념이며 다른 도구와 마찬가지로 당신이 무엇을하고 있는지 안다면 당신의 삶을 더 쉽게 만들 수 있습니다. 매크로의 문제는 매크로가 c / c ++ 코드보다 먼저 처리되고 결함이있을 수 있고 명백한 컴파일러 오류를 일으킬 수있는 새 코드를 생성한다는 것입니다. 밝은면에서는 코드를 깔끔하게 유지하고 올바르게 사용하면 많은 타이핑을 절약 할 수 있으므로 개인 취향에 따라 결정됩니다.


또한 다른 답변에서 지적했듯이 잘못 설계된 전 처리기 정의는 유효한 구문이지만 의미 론적 의미가 다른 코드를 생성 할 수 있습니다. 즉, 컴파일러가 불평하지 않고 코드에 버그를 도입하여 찾기가 더 어려워집니다.
Sandi Hrvić 2012

3

C / C ++의 매크로는 버전 제어를위한 중요한 도구 역할을 할 수 있습니다. 매크로의 사소한 구성으로 동일한 코드를 두 클라이언트에 전달할 수 있습니다. 나는 같은 것을 사용한다

#define IBM_AS_CLIENT
#ifdef IBM_AS_CLIENT 
  #define SOME_VALUE1 X
  #define SOME_VALUE2 Y
#else
  #define SOME_VALUE1 P
  #define SOME_VALUE2 Q
#endif

이러한 종류의 기능은 매크로 없이는 쉽게 불가능합니다. 매크로는 실제로 훌륭한 소프트웨어 구성 관리 도구이며 코드 재사용을위한 바로 가기를 만드는 방법이 아닙니다. 매크로에서 재사용을 목적으로 함수를 정의하면 확실히 문제가 발생할 수 있습니다.


컴파일하는 동안 cmdline에 매크로 값을 설정하여 하나의 코드베이스에서 두 가지 변형을 빌드하는 것은 정말 좋습니다. 적당히.
kevinf

1
어떤 관점에서 보면이 사용법은 가장 위험한 것입니다. 도구 (IDE, 정적 분석기, 리팩토링)는 가능한 코드 경로를 파악하는 데 어려움을 겪습니다.
erenon

1

문제는 매크로가 컴파일러에 의해 잘 최적화되지 않았고 읽기 및 디버그하기에 "추악"하다는 것입니다.

종종 좋은 대안은 일반 함수 및 / 또는 인라인 함수입니다.


2
매크로가 잘 최적화되어 있지 않다고 생각하는 이유는 무엇입니까? 간단한 텍스트 대체이며 결과는 매크로없이 작성된 코드만큼 최적화됩니다.
Ben Voigt 2012

@BenVoigt 그러나 그들은 의미론을 고려하지 않으며 이것은 "최적화되지 않은"것으로 간주 될 수있는 무언가로 이어질 수 있습니다 ... 적어도 이것은 그 stackoverflow.com/a/14041502/1849534
user1849534

1
@ user1849534 : 컴파일 컨텍스트에서 "최적화"라는 단어가 의미하는 것은 아닙니다.
Ben Voigt 2012

1
@BenVoigt 정확히 매크로는 텍스트 대체 일뿐입니다. 컴파일러는 코드를 복제 할뿐 성능 문제는 아니지만 프로그램 크기를 늘릴 수 있습니다. 프로그램 크기 제한이있는 일부 상황에서는 특히 그렇습니다. 일부 코드는 매크로가 너무 많아 프로그램 크기가 두 배가됩니다.
Davide Icardi

1

전 처리기 매크로는 다음과 같은 목적으로 사용될 때 악의가 없습니다.

  • #ifdef 유형의 구성을 사용하여 동일한 소프트웨어의 다른 릴리스를 작성합니다 (예 : 다른 지역의 창 릴리스).
  • 코드 테스트 관련 값을 정의합니다.

대안- 비슷한 목적으로 ini, xml, json 형식의 구성 파일을 사용할 수 있습니다. 그러나 그것들을 사용하면 전 처리기 매크로가 피할 수있는 코드에 런타임 효과가 있습니다.


1
C ++ 17 constexpr if + "config"constexpr 변수를 포함하는 헤더 파일은 #ifdef를 대체 할 수 있습니다.
Enhex
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.