#27. [모던 C++] 모듈(module)(C++20)
개요
C++ 는 다음의 전처리, 컴파일, 링크의 3가지 과정을 거쳐서 프로그램을 빌드합니다.
이중 전처리 과정은 다음의 문제점이 있습니다.
-
용량 비대화에 따른 컴파일 속도 문제
#include를 하고 나면 소스 파일은 엄청 거대해집니다.
1 2 3 4 5 6 7
// helloworld.cpp #include <iostream> int main() { std::cout << "Hello World" << std::endl; return 0; }
상기 코드에는
cout
과endl
이 있어서#include <iostream>
이 필요합니다. 이에 전처리를 해야 하는데요,다음과 같이
-E
옵션으로 전처리 결과를 출력할 수 있고, 해당 내용을-o
옵션으로 파일에 저장할 수 있습니다.1
F:\Data\language_test\test\module>g++ -E helloworld.cpp -o helloworld.out
helloworld.out
파일의 결과는 놀랍게도 943kbyte나 됩니다. 대략 1000배가 늘었습니다. 이를 다 일일이 기계어로 번역하다 보니, 단순히Hello World
만 출력하는 별것 아닌 코드이지만 컴파일 시간이 좀 걸리게 됩니다.아마도
iostream
이 또다른 파일을 #include 하고, #include한 파일에서 또 다른 파일들을 #include 하다 보니 벌어진 일이겠죠. 파일 구성에서 말씀드린 것처럼, 헤더 파일에서 다른 헤더 파일을 #include하는 것을 최소화 해서 해결하면 되겠지만, 아무래도 한계가 있습니다. -
#include 순서에 따른 종속성 문제
#define이나 #include는 해당 위치에 소스 코드를 대체하는 것이기 때문에, 어떤 것을 먼저 대체했는지에 따라 다른 결과가 나올 수 있습니다. 또한 정적 변수의 초기화 순서와 같은 문제가 있을 수도 있습니다.
-
선언과 정의를 분리하는 파일 구성의 불편함
컴파일 속도 향상을 위해 선언과 정의를 분리하는데요, 소스 코드 관리가 상당히 번거롭습니다. 컴파일 속도와 구현 코드의 은닉이 더 중요하니 어쩔 수 없이 참고 사용하죠.
-
기호 충돌
정의가 중복되면 충돌이 납니다. 따라서 전역 변수를
extern
으로 선언한다던지, 함수를 inline으로 정의한다던지 해야 합니다.
C++20 부터는 모듈이 추가되어 전처리 사용 방식을 개선하여 컴파일 속도를 향상시키고, #include 순서에 따른 종속성 문제, 선언과 정의 분리 구성의 불편함, 기호 충돌 문제를 해결했습니다.
모듈을 사용하는 방식은 컴파일러마다 다릅니다. 자세한 내용은 컴파일러 도움말을 참조 하세요.
항목 | MSVC | CLang | GCC |
---|---|---|---|
모듈 파일 확장자 | ixx |
cppm 또는 cpp |
특별한 확장자 없음 |
컴파일 옵션 | /interface |
-emit-module-interface |
-fmodules-ts |
다음은 MyMoudule.cpp
와 main.cpp
을 분리한 예입니다.
MyMoudule.cpp
에서는 내부적으로 cout
을 사용하기 위해 #include <iostream>
를 사용하며, 외부에서 사용할 수 있도록 Func_20()
함수를 export로 내보내기 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ----
// MyModule.cpp 파일에서
// ----
module; // 전역 모듈 조각
#include <iostream> // 모듈에서 사용하는 헤더 파일
export module MyModule_20; // 내보내기할 모듈 선언
void Print() {
std::cout << "MyModule_20 : Print()" << std::endl;
}
export void Func_20() { // 내보내기 선언
Print();
}
main.cpp
에서는 import을 이용하여 MyModule_20
모듈을 가져오며, 내보내기된 Func_20()
함수를 사용합니다. 이때 Print()
함수는 내보내기 되지 않았으므로 사용할 수 없습니다.
1
2
3
4
5
6
7
8
9
10
// ----
// main.cpp 파일에서
// ----
import MyModule_20; // 가져오기 선언
int main() {
Func(); // 모듈에서 Export한 함수를 사용합니다.
// Print(); // (X) 컴파일 오류. export 한 함수가 아닙니다.
return 0;
}
GCC에서는 다음과 같이 빌드합니다.
1
F:\Data\language_test\test\module>g++ -std=c++20 -fmodules-ts MyModule.cpp main.cpp -o MyModule
export(모듈 내보내기) 와 import(모듈 가져오기)
모듈 선언인 export module Module_20;
이하에 export로 내보낼 항목을 작성합니다. 개별로 내보낼 수도 있고, export {}
로 그룹지어 내보낼 수도 있으며, 네임스페이스를 통째로 내보낼 수도 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ----
// Test_ExportImport.cpp 파일에서
// ----
export module MyModule_20; // 내보내기할 모듈 선언
// 개별 내보내기
export void Func1() {}
// 그룹 내보내기
export {
void Func2() {}
void Func3() {}
}
// 네임스페이스 내보내기
export namespace MyLib {
void Func4() {}
void Func5() {}
}
// 템플릿 함수 내보내기
export template<typename T>
void Func6(T val) {}
// 클래스 내보내기
export class T {};
가져오기시에는 import로 모듈만 가져오면, 모듈에서 export한 모든 항목들을 사용할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ----
// Test_ExportImport_main.cpp 파일에서
// ----
import MyModule_20; // 가져오기 선언
int main() {
Func1();
Func2();
Func3();
MyLib::Func4();
MyLib::Func5();
Func6<int>(10);
Func6<double>(10.0);
T obj;
return 0;
}
import 헤더 파일
특별히 #include를 import로 변경할 수 있습니다.
(GCC 12.3.0 에서 컴파일되지 않습니다. 뭐가 잘못됐는지 좀더 확인해 봐야 합니다. https://build2.org/blog/build2-cxx20-modules-gcc.xhtml#header-units 참고)
1
2
3
4
5
6
7
8
// #include <iostream>
import <iostream>;
int main() {
std::cout << "Test Import header" << std::endl;
return 0;
}
모듈 인터페이스와 구현 분리
export로 외부로 내보낼때 인터페이스와 구현을 분리할 수 있습니다.
다음의 Test_Interface.cpp
에서 Func()
함수의 선언만 내보내고,
1
2
3
4
5
// ----
// Test_Interface.cpp 파일에서
// ----
export module MyModule_20;
export void Func(); // 인터페이스 부분은 함수 선언만 합니다.
Test_Implement.cpp
에서 Func()
함수를 정의합니다. 이때 MyModule_20
과 Func()
에는 export를 붙이지 않습니다.
1
2
3
4
5
// ----
// Test_Implement.cpp 파일에서
// ----
module MyModule_20; // export를 붙이지 않았습니다.
void Func() {} // export를 붙이지 않습니다.
다음과 같이 MyModule_20
을 import하여 사용할 수 있습니다.
1
2
3
4
5
6
7
8
9
// ----
// Test_Interface_main.cpp 파일에서
// ----
import MyModule_20;
int main() {
Func(); // 모듈에서 Export한 함수를 사용합니다.
return 0;
}
전역 모듈 조각과 개인 모듈 조각
전역 모듈 조각은 모듈의 상단에 module;
로 표시하며, 모듈에서 사용하는 헤더 파일을 #include합니다.
개인 모듈 조각은 모듈의 하단에 module : private;
로 표시하며, 모듈 인터페이스와 구현을 하나의 파일에 작성할 수 있게 해줍니다. 즉, 함수 선언에는 export를 작성하고, module : private;
에 실제 구현을 합니다.
전체적인 모듈 파일 레이아웃은 다음과 같습니다.(GCC 12.3.0 에서 module : private;
은 아직 구현되지 않았다는 컴파일 오류가 발생합니다.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ----
// Test_Fragment.cpp 파일에서
// ----
module; // 전역 모듈 조각(Global module fragment)
#include<iostream> // 모듈에서 사용하는 헤더 파일
export module MyModule_20; // 모듈 선언
export void Func(); // 인터페이스만 작성합니다.
module : private; // 구현을 작성합니다.
void Print() {
std::cout << "MyModule_20 : Print()" << std::endl;
}
void Func() {
Print();
}
다음과 같이 MyModule_20
을 import하여 사용할 수 있습니다.
1
2
3
4
5
6
7
8
9
// ----
// Test_Fragment_main.cpp 파일에서
// ----
import MyModule_20;
int main() {
Func(); // 모듈에서 Export한 함수를 사용합니다.
return 0;
}
모듈 분할
하나의 모듈을 여러개로 나누어 관리할 수 있습니다.
다음예는 MyModule_20
을 Part1
, Part2
, Part3
으로 나누어 관리하는 예입니다. Part1
과 Part2
는 외부에서도 사용하고, Part3
은 MyModule_20
내부에서만 사용합니다.
Test_Part.cpp
는 모듈의 인터페이스로서 Part1
과 Part2
를 import한뒤 export합니다. 다른 파트를 표현할때 :Part1;
와 같이 모듈명 없이 작성합니다.
1
2
3
4
5
6
7
// ----
// Test_Part.cpp 파일에서
// ----
export module MyModule_20;
export import :Part1; // 각 파트를 import한뒤 export 합니다.
export import :Part2;
Test_Part1.cpp
, Test_Part2.cpp
는 각 파트를 정의하고 export합니다.
이때 Part1
에서 Part3
을 사용하는데요, import :Part3;
하여 가져오기를 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ----
// Test_Part1.cpp 파일에서
// ----
export module MyModule_20:Part1;
import :Part3;
export void Part1() {
Part3();
}
// ----
// Test_Part2.cpp 파일에서
// ----
export module MyModule_20:Part2;
export void Part2() {}
Part3
은 외부에 내보내지 않고 MyModule_20
내애서만 사용할 건데요, 모듈의 각 파트간에는 서로 사용할 수 있기 때문에 굳이 export를 하지 않아도 됩니다. 따라서, module MyModule_20:Part3;
와 같이 export없이 작성합니다.
1
2
3
4
5
// ----
// Test_Part3.cpp 파일에서
// ----
module MyModule_20:Part3; // MyModule_20에서만 사용하므로 export 하지 않습니다.
void Part3() {} // MyModule_20에서만 사용하므로 export 하지 않습니다.
다음과 같이 MyModule_20
을 import하여 사용할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
// ----
// Test_Part_main.cpp 파일에서
// ----
import MyModule_20; // MyModule_20.Part1, MyModule_20.Part2를 사용할 수 있습니다.
int main() {
Part1();
Part2();
return 0;
}
컴파일할 때 순서에 주의해야 합니다. 종속성 있는 파트들이 먼저 컴파일되어야 합니다. 따라서 Test_Part3.cpp
는 Test_Part1.cpp
보다 먼저 컴파일되어야 하고, Test_Part.cpp
는 Test_Part3.cpp Test_Part1.cpp Test_Part2.cpp
보다 나중에 컴파일 되어야 합니다.
1
F:\Data\language_test\test\module>g++ -std=c++20 -fmodules-ts Test_Part3.cpp Test_Part1.cpp Test_Part2.cpp Test_Part.cpp Test_Part_main.cpp -o Test_Part_main
하위 모듈
모듈의 이름에는 .
을 사용할 수 있는데요, 이를 이용해서 관용적으로 하위 모듈처럼 관리할 수 있습니다. MyModule_20.Sub1
, MyModule_20.Sub2
, MyModule_20.Sub3
처럼요. 하지만 그냥 이름이 다른 모듈들일 뿐입니다.
다음은 모듈 분할에서의 예제를 하위 모듈로 변경한 예입니다. 서로 다른 모듈간에 사용하려면 export
되어야 하기 때문에, MyModule_20.Sub3
도 export 되었으며, 이에 따라 main()
에서도 MyModule_20.Sub3
import을 하여 사용할 수 있습니다. 은닉성을 위해 모듈 분할을 사용하는게 더 좋습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ----
// Test_Sub.cpp 파일에서
// ----
export module MyModule_20;
export import MyModule_20.Sub1;
export import MyModule_20.Sub2;
// ----
// Test_Sub1.cpp 파일에서
// ----
export module MyModule_20.Sub1;
import MyModule_20.Sub3;
export void Sub1() {
Sub3();
}
// ----
// Test_Sub2.cpp 파일에서
// ----
export module MyModule_20.Sub2;
export void Sub2() {}
// ----
// Test_Sub3.cpp 파일에서
// ----
export module MyModule_20.Sub3; // (△) 비권장. Sub1에서 사용하기 위해 export를 합니다.
export void Sub3() {} // (△) 비권장. Sub1에서 사용하기 위해 export를 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
// ----
// Test_Sub_main.cpp 파일에서
// ----
import MyModule_20; // MyModule_20.Sub1, MyModule_20.Sub2를 사용할 수 있습니다.
import MyModule_20.Sub3; // (△) 비권장. export되었기에 main에서도 사용할 수 있습니다.
int main() {
Sub1();
Sub2();
Sub3(); // (△) 비권장. export되었기에 main에서도 사용할 수 있습니다.
return 0;
}
모듈간 충돌
모듈에서 내보내는 개체가 동일하면 무얼 사용해야 할지 모호하기 때문에 컴파일 오류가 발생합니다.
1
2
3
4
5
6
7
8
9
10
11
// ----
// Test_My.cpp 파일에서
// ----
export module MyModule_20;
export void Func() {}
// ----
// Test_Your.cpp 파일에서
// ----
export module YourModule_20;
export void Func() {} // MyModule_20의 Func() 과 동일합니다.
1
2
3
4
5
6
7
8
9
10
// ----
// Test_MyYour_main.cpp 파일에서
// ----
import MyModule_20;
import YourModule_20;
int main() {
Func(); // (X) 컴파일 오류. MyModule_20 과 YourModule_20 모듈에 모두 있습니다.
return 0;
}
따라서, 네임스페이스를 사용하시는게 좋습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ----
// Test_My.cpp 파일에서
// ----
export module MyModule_20;
export namespace MyLib {
void Func() {}
}
// ----
// Test_Your.cpp 파일에서
// ----
export module YourModule_20;
export namespace YourLib {
void Func() {}
}
// ----
// Test_MyYour_main.cpp 파일에서
// ----
import MyModule_20;
import YourModule_20;
int main() {
MyLib::Func();
YourLib::Func();
return 0;
}
댓글남기기