질문에 대한 답변
질문의 역사
C++20 이전에 C++ 컴파일 모델은 전처리기 지시문을 통한 텍스트 포함에 의존했습니다. 헤더 파일이 포함되면, 전처리기는 해당 헤더의 텍스트를 포함하는 파일에 문자 그대로 복사했습니다. 이 메커니즘은 헤더에서 정의된 매크로가 이를 포함하는 모든 번역 단위의 전역 네임스페이스로 누수되는 원인이 되어, 진단하기 어려운 미묘한 버그와 이름 충돌을 초래했습니다.
문제
매크로 누수는 대규모 코드베이스에서 유지 관리의 악몽을 초래했습니다. 서드파티 라이브러리에서 정의된 매크로가 소비자 코드에서 키워드나 일반 식별자를 조용히 재정의할 수 있어, 컴파일 실패나 실제 원인과는 관련이 없어 보이는 런타임 오류를 발생시킬 수 있었습니다. 전통적인 우회 방법인 #undef 가드는 수동적이고 오류가 발생하기 쉬우며 복잡한 의존성 그래프에 대해서는 확장되지 못했습니다. 근본적인 문제는 전처리기가 범위나 인터페이스 경계에 대한 개념이 없었다는 것입니다.
해결책
C++20 모듈은 전처리기 레벨이 아닌 언어 레벨에서 작동하는 의미론적 가져오기 메커니즘을 도입합니다. import module_name;으로 모듈을 가져올 때, 컴파일러는 가져오는 번역 단위의 전처리기 지시문을 실행하지 않고 모듈의 내보내기 인터페이스를 처리합니다. 모듈 내에서 정의된 매크로는 명시적으로 내보내지 않는 한 해당 모듈의 구현에 대해서만 비공식적입니다. 이 속성은 매크로가 번역 단위 경계를 넘어 누수되지 않도록 보장하여 진정한 캡슐화와 이름 오염 방지를 제공합니다.
// mathlib.cpp (모듈 구현) module; #define INTERNAL_CALC_FACTOR 3.14 // 비공식 매크로, 누수되지 않음 export module mathlib; export double compute(double x) { return x * INTERNAL_CALC_FACTOR; } // main.cpp (소비자) import mathlib; // INTERNAL_CALC_FACTOR는 여기서 보이지 않음 // #ifdef INTERNAL_CALC_FACTOR는 false일 것입니다. int main() { double result = compute(10.0); // 잘 작동 }
실제 상황
한 금융 거래 회사는 수백 개의 모듈에 걸쳐 수백만 줄의 코드로 구성된 대규모 코드베이스를 유지 관리했습니다. 그들은 공개 헤더에서 MIN과 MAX와 같은 매크로를 정의한 레거시 수학 라이브러리에 의존했습니다. 이러한 매크로는 표준 라이브러리 함수와 min 및 max를 변수 이름 또는 함수 템플릿으로 사용하는 서드파티 JSON 파싱 라이브러리와 자주 충돌했습니다.
첫 번째로 고려된 접근 방법은 모든 서드파티 헤더를 #pragma once 스타일 가드로 래핑하고, 각 포함 후 문제의 매크로를 수동으로 #undef하는 것이었습니다. 이는 개발자가 어떤 헤더가 어떤 매크로를 정의했는지 기억하고, 각 포함 후 정리해야 했습니다. 이 접근 방법은 실수 한 번으로 관련 없는 부분에서 실패를 초래할 수 있어 취약했습니다. 또한 전처리기가 동일한 헤더 텍스트를 번역 단위 간에 반복 처리함으로써 컴파일 시간이 크게 증가했습니다.
두 번째로 고려된 접근 방법은 매크로 대신 인라인 함수와 템플릿을 사용하는 것을 수학 라이브러리로 변환하는 것이었습니다. 이 방법은 누수 문제를 해결했지만 레거시 라이브러리를 광범위하게 수정해야 했습니다. 수학 라이브러리는 여러 팀에서 사용되었고, 이를 변경하는 것은 특정 매크로 평가 의미론이나 부작용에 의존하는 기존 계산을 손상시킬 위험이 있었습니다. 리팩토링 작업에는 6개월이 소요될 것으로 예상되었고, 거래 플랫폼에 너무 위험한 것으로 간주되었습니다.
선택된 해결책은 C++20 모듈로 전환하는 것이었습니다. 팀은 수학 라이브러리를 변환하여 수학적 함수를 내보내면서 매크로를 모듈 구현 내에서 내부로 유지했습니다. #include <mathlib.h> 대신 import mathlib;를 사용함으로써 소비하는 번역 단위는 더 이상 MIN 및 MAX 매크로를 보지 않게 되었습니다. 이 접근 방법은 라이브러리 구현 중에서 최소한으로 변경해야 하며 내보내기 문을 추가하고 헤더를 모듈 인터페이스 단위로 변환하는 것만 필요했습니다. 마이그레이션은 6개월이 아닌 2주가 소요되었습니다. 그 결과는 코드베이스 전반의 매크로 관련 이름 충돌의 제거와 모듈의 컴파일된 인터페이스 덕분에 15%의 컴파일 시간 감소였습니다.
후보들이 자주 놓치는 점
모듈 인터페이스 단위의 컴파일된 이진 형식은 텍스트 헤더 포함과 비교하여 매크로 누수를 어떻게 방지합니까?
후보자들은 종종 C++20 모듈이 매크로의 내보내기_EXPORT_MySQL_AutoComment(is_active=true) 기능을 팔아주는지 헷갈πά 합니다. C++20은 export import를 사용하여 매크로를 내보내는 것을 허용하지만, 이러한 매크로는 모듈을 가져오는 코드에만 영향을 미치며 해당 가져오기 범위를 넘어 누수되지 않습니다.
#include와 달리 매크로는 번역 단위에서 명시적으로 정의해두지 않으면 사라지고, 모듈의 내보낸 매크로는 가져오는 번역 단위가 해당 모듈에 노출된 범위로 제한됩니다. 전처리기는 가져온 매크로를 가져오는 지점에서 정의된 것처럼 취급하지만, 이는 텍스트 포함과 동일한 방식으로 후속 가져오기나 전역 전처리 상태에 영향을 미치지 않습니다.
또한, 여러 모듈이 충돌하는 매크로를 내보낼 경우, 충돌은 가져오기 시점에 감지되며 나중에 컴파일에서 조용한 재정의 오류를 발생시키지 않습니다. 이 범위 지정 동작은 텍스트 포함이 결여한 위생성을 제공하여 매크로가 적절한 네임스페이스 범위 개체처럼 동작하게 합니다.
모듈이 전처리기로부터 독립함으로써 빌드 시스템 통합 및 의존성 스캔에 미치는 영향은 무엇입니까?
후보자들은 종종 C++20 모듈이 빌드 시스템이 컴파일이 시작되기 전에 모듈 의존성을 이해해야 한다고 놓치는 경우가 많습니다. 이는 헤더에서는 의존성이 컴파일 도중 발견되는 것과 다릅니다. 모듈은 텍스트 파일이 아닌 컴파일 단위이기 때문에 빌드 시스템은 모듈 인터페이스 단위를 파싱하여 어떤 것을 내보내고 가져오는지를 결정해야 합니다.
이것은 두 단계의 빌드 프로세스를 요구합니다: 첫째, 모듈 인터페이스 단위를 스캔하여 의존성 그래프를 구축하고, 그 다음 의존성 순서로 컴파일합니다. 전처리기 독립성 덕분에 헤더 포함을 위한 전통적인 #ifdef 가드는 무관하게 되며, 모듈 인터페이스의 매크로 기반 구성이 제한됩니다. 빌드 시스템은 단순한 소스 파일이 아닌 컴파일된 모듈 아티팩트(BMI - 이진 모듈 인터페이스)를 추적해야 합니다.
이것은 의존성 추적 및 증분 빌드가 작동하는 방식을 근본적으로 변경합니다. 빌드 시스템은 이제 BMI 파일을 중간 아티팩트로 관리해야 하며, 이는 자체 의존성 체인을 가지고 있으며 모듈 인식 컴파일 그래프를 지원하기 위해 CMake 또는 Bazel과 같은 빌드 도구의 업데이트가 필요합니다.