모듈(module) 이란?
C++20 이전의 C++에서는 필요한 함수 또는 클래스를 불러오기 위해 #include 전처리문을 이용해 왔다. 이런 헤더 파일 방식의 문제는..많지만 그 중에 필자가 가장 크리티컬하게 생각하는 부분은 #include 전처리문을 가리키고 있던 파일의 내용 그대로 치환해버려 헤더에 있는 필요하든 필요하지 않든 상관 없이 정의 되어있는 모든 기능을 포함하게 된다는 것이다. 예를 들어 cmath 헤더 파일에서 정작 내가 필요한 기능은 acos함수 하나 뿐이지만, acos를 사용하기 위해서 나는 헤더에 정의된 모든 함수들을 인클루드하고 컴파일 해야만 한다.
이미 현재 다른 언어들에서는 필요한 기능만을 가져 올 수 있는 기능을 제공하고 있지만 C++은 이번 C++20 스펙의 module을 통해 필요한 것만 가져올 수 있는 기능을 제공한다.
기존 C++시스템과 module 도입 이후의 차이와 이점에 대해서 [여기]에 정리 되어 있으니 살펴 보도록 하자.
모듈 코드 작성하기
앞에서 알아 본바와 같이 모듈의 의도와 개념은 간단 명료하다. 이제 모듈을 사용하기 위해 필요한 것들을 알아 보자. 모듈을 사용하기 위해 우리가 알아야 할 것도 간단 명료하다.
module, import, export 이 세가지 키워드를 기억하자
- module : 모듈의 이름을 지정
eg) module Math : 모듈의 이름은 'Math'이다. - import : 가져올 모듈의 이름을 지정
eg) import Math : 가져올 대상 모듈의 이름은 'Math'이다. - export : 모듈에서 내보낼 기능(함수)의 인터페이스를 지정
eg) export int sum(int, int) : 내보낼 함수의 이름은 sum이고 리턴 타입은 int, 인자는 int, int다.
모듈 선언(declaration)
기존 #include 전처리 방식은 컴파일러와 상관 없이 선언은 헤더 파일에 위치하고 구현은 .cpp 파일 적성하는 방식이었다면 module은 각 컴파일러마다 각각 다른 방식으로 작성된다.
cl.exe(Microsoft 비주얼 스튜디오)
cl.exe는 비주얼 스튜디오의 컴파일러 이름이다. cl.exe는 모듈을 선언하기 위해 확장자가 ixx인 모듈 인터페이스 파일을 사용한다. 모듈을 만들기 위해 가장 먼저 할 일은 프로젝트에서 아래와 같이 'C++ 모듈 인터페이스 단위(.ixx)'를 선택하여 파일을 생성한다.
확장자가 .ixx인 모듈 인터페이스 파일을 만들고 내보낼 모듈의 이름과 인터페이스를 작성한다.
// ModuleA.ixx 모듈 인터페이스 파일
export module ModuleA; // 내보낼 모듈의 이름 지정
namespace Foo
{
export int MyIntFunc() // 모듈에서 내보낼 기능(함수)의 인터페이스를 지정
{
return 0;
}
export double MyDoubleFunc()
{
return 0.0;
}
void InternalMyFunc() // 모듈 내부에서만 사용하는 함수
{
}
}
- 3라인의 'export module ModuleA'는 우리가 함수나 변수를 선언하는 것과 같이 모듈을 만들겠다는 선언이다.
- 7라인과 12라인의 export가 선언되어 있는 MyIntFunc()와 MyDoubleFunc()는 다른 파일에서 이 모듈을 임포트(import)했을 때 사용할 수 있는 함수라는 것을 의미한다.
- 17라인의 InternalMyFunc() 함수는 모듈 내부에서만 사용되고 이 모듀을 임포트하는 곳에서는 사용하지 못한다.
- 위 예제와 같이 선언과 구현을 ixx파일에 모두 작성할 수도 있고 다음 예제와 같이 기존 .h, .cpp 구조 처럼 나눠서 작성할 수도 있다.
// 선언만 있는 ModuleA.ixx 모듈 인터페이스 파일
export module ModuleA; // 내보낼 모듈의 이름 지정
namespace Foo
{
export int MyIntFunc();
export double MyDoubleFunc();
void InternalMyFunc();
}
// ModuleA.cpp 모듈 구현 파일
module ModuleA; // 시작 부분에 모듈 선언을 배치하여 파일 내용이 명명된 모듈(ModuleA)에 속하도록 지정
namespace Foo
{
int MyIntFunc() // 구현에서는 export 키워드가 빠진다.
{
return 0;
}
double MyDoubleFunc()
{
return 0.0;
}
void InternalMyFunc()
{
}
}
Visual Studio 2019 버전 16.2에서는 모듈이 컴파일러에서 완전히 구현 되지 않았기 때문에 모듈을 사용하기 위해서는 /experimental:module 스위치를 활성화 해야 한다. 하지만 이 글을 적는 시점에서 최신버전 컴파일러는 모든 스펙을 다 구현하였으므로 모듈을 사용기 위해서는 최신 버전으로 업데이트할 필요가 있다.
module, import 및 export 선언은 C++20에서 사용할 수 있으며 C++20 컴파일러를 사용한다는 스위치 활성화가 필요하다(/std:c++latest ). 보다 자세한 사항은 [C++20] 컴파일 항목을 참고 하도록 하자.
참고로 필자의 경우 x86 32-bit 프로젝트를 생성해서 모듈을 사용하려고 했을 때 제대로 컴파일 되지 않았다. 비주얼 스튜디오에서 모듈을 사용하기 위해서는 64-bit 모드로 프로젝트를 설정해야 한다.
gcc
gcc의 경우 모듈을 선언하기 위해 .cpp 파일을 사용하지만 컴파일 시 -fmodules-ts 옵션을 이용해 컴파일 해야 한다. gcc에서 모듈을 사용하기 위해서는 gcc 버전 11이상이 필요하다. gcc 11의 설치 방법에 대해서는 [여기]를 참고 하도록 한다. gcc 컴파일에 대한 자세한 방법은 [여기]를 참고하자.
Global Module Fragment
적절하게 번역할 단어를 찾지 못해서 그대로 글로벌 모듈 프래그먼트(Global Module Fragment)라고 부르도록 하겠다. 글로벌 모듈 프래그먼트는 #include와 같은 전처리 지시자를 적어 주는 곳이라 생각하면 된다. 글로벌 모듈 프래그먼트는 익스포트(export)되지 않는다. 여기에 #include를 이용해 헤더를 불러왔다고 하더라도 모듈을 가져다 쓰는 곳에서 include의 내용를 볼 수 없다는 뜻이다.
// math1.ixx
module; // global module fragment (1)
#include <numeric> // #include는 여기 들어간다
#include <vector>
export module math; // module declaration (2)
export int add(int fir, int sec){
return fir + sec;
}
export int getProduct(const std::vector<int>& vec) {
return std::accumulate(vec.begin(), vec.end(), 1, std::multiplies<int>());
}
모듈 불러오기(import)
모듈을 불러와 사용하는 방법은 기존의 include와 매우 비슷하다. #include 대신 import 키워드를 사용한다
import ModuleA; // 모듈 임포트
#include <iostream>
int main()
{
std::cout << Foo::MyIntFunc() << std::endl;
return 0;
}
마치며
필자의 경우 비주얼 스튜디오에서는 #include, import 순서에 상관 없이 정상적으로 컴파일 되었지만, gcc에선 헤더와 모듈의 순서에 따라 컴파일 에러가 발생했었다(내가 뭔가를 잘못해서 그럴 수도 있지만..). 모듈을 임포트 후 shared_ptr을 위해 memory 헤더를 인클루드하면 컴파일 오류가 발생하고, 헤더를 먼저 인클루드 후 모듈을 임포트하면 정상적으로 컴파일 되었다.
// compile : g++ -std=c++20 -fmodules-ts hello.cc main.cc
// import hello; // Compile Error!!
// #include <memory>
#include <memory> // Compile Success
import hello;
int main()
{
std::shared_ptr<int> i = std::shared_ptr<int>(new int);
greeter("world");
return 0;
}
위 예제의 전체 프로젝트 코드는 [여기]에서 확인 할 수 있다.