매크로에 대처하는 우리의 자세

  • Post Author:
  • Post Category:칼럼
  • Post Comments:0 Comments
  • Post last modified:February 8, 2020

윈도우 프로그래밍에서 빼놓지 않고 등장하는 헤더가 무엇일까? Hello World!를 찍을 때 필요한 <stdio>나 <iostream>? C/C++의 세계를 아울러 보면 그럴지도 모른다. 그러나 윈도우 프로그래밍에만 등장하는 헤더를 놓고 따진다면 <windows.h>도 꽤 강력한 후보이리라 생각한다. <windows.h>는 자주 쓰는 각종 헤더와 타입, 매크로 등을 한데 묶었기 때문에 보통 미리 컴파일된 헤더에 놓고 이용한다. 그런데 이 헤더가 항상 아름답지는 않다. 특히 자주 쓰는 이름을 매크로 이름으로 정의한 탓에 신경을 박박 긁기도 한다. min, max가 그런 매크로 중 하나다.

// windows.h 의 포함된 windef.h 에서
#ifndef max
#define max(a,b)            (((a) > (b)) ? (a) : (b))
#endif

#ifndef min
#define min(a,b)            (((a) < (b)) ? (a) : (b))
#endif

<windows.h>의 min, max는 두 매개변수 중 작은 쪽이나 큰 쪽을 반환한다. 겉보기엔 쓸모 있어 보이지만 실제론 그렇지도 않다. C++ 표준 라이브러리에 동일한 역할을 하는 함수가 있기 때문이다.

std::min(1, 2); // 매개변수 중 작은 쪽을 반환한다.
std::max(1, 2); // 매개변수 중 큰 쪽을 반환한다.

std::numeric_limits<int>::min(); // int 타입의 최소값을 반환한다.
std::numeric_limits<int>::max(); // int 타입의 최대값을 반환한다.

표준 라이브러리의 min, max는 인라인 템플릿 함수라서 매크로보다 성능이 떨어지지도 않고 매크로 때문에 발생하는 문제도 겪지 않는다. 그런데 <windows.h>에서 min과 max를 매크로로 선언했기 때문에 위의 코드는 컴파일되지 않는다.

매크로의 단점

    #define max(a,b)            (((a) > (b)) ? (a) : (b))
    
    inline int max2(int a, int b)
    {
        return a > b ? a : b;
    }

    int a = 1;
    int b = 2;
    int retValue1 = max(a, ++b);
    int retValue2 = max2(a, ++b);
    

변수 retValue1과 retValue2의 값은 무엇이 될까? 매크로 접해보지 않았다면 값이 똑같으리라 생각할지 모른다. 그러나 현실은 처참하다. retValue1은 4가 되고, retValue2는 3이 된다. max2(a, ++b)는 사실상 다음과 같이 변환되기 때문이다.

    a > ++b ? a : ++b
    
warning C4003: \'max\' 매크로의 실제 매개 변수가 부족합니다.

이 문제를 해결하는 방법은 많다. 가장 쉬운 방법은 <windows.h>에서 min, max를 선언하지 않는 것이다. 그러려면 <windows.h>를 참조하기 전에 NOMINMAX를 선언하면 된다.

#define NOMINMAX // min, max 매크로 없애기
#include <windows.h>

이렇게 하면 min, max 매크로를 선언하지 않는다. 그러나 min, max 매크로를 그냥 두고 싶을지도 모른다. 그런 경우에 대비하여 <windows.h>에는 std::min과 std::max에 대응하는 _cpp_min, _cpp_max가 있다.

_cpp_min(a, b);
_cpp_max(a, b);

편리한 방법이지만 해당 소스 파일을 다른 환경, 그러니까 GNU C++ 컴파일러를 써서 컴파일해야 할 때는 소용이 없다. 교차 플랫폼을 생각하면 환경에 따라 _cpp_min 매크로를 직접 선언해야 한다. 물론 윈도우 환경만 생각하면 가장 쉬운 해결책 중 하나다.

템플릿 타입을 명시함으로써 min, max 매크로의 치환을 막는 방법도 있다.

std::min<int>(a, b);
std::max<int>(a, b);

std::min, std::max에 국한해선 이 방법이 좋다. 템플릿 함수이기 때문이다. 그러나 <windows.h>가 아니더라도 외부 라이브러리의 매크로 때문에 고생하는 일은 흔하다. 그 중 상당수는 템플릿 함수가 아니다. 만약 여러 상황에 두루 적용 가능한 수단이 필요하다면 다음의 사례를 고려해보자.

// 매크로 선언 무효화하기
#undef min
#undef max

// 함수를 ()로 감싸 매크로 치환 대상에서 제외하기
(std::min)(a, b);
(std::max)(a, b);

// 빈 매크로를 이용하기
#define BOOST_PREVENT_MACRO_SUBSTITUTION
std::min BOOST_PREVENT_MACRO_SUBSTITUTION (a, b);
std::max BOOST_PREVENT_MACRO_SUBSTITUTION (a, b);
Buy me a coffeeBuy me a coffee

최 재훈

Kubernetes, DevSecOps, Golang, 지속적인 통합 등 다양한 주제에 관심이 많다.