Hello, Modules!
1. Module 사용해보기
아직은 visual studio 에서, Compile과정은 비표준입니다.
Module을 사용 전, 세팅해주어야 하는게 있습니다.
1. 속성 -> C/C++ -> 언어 에서 C++ 언어 표준을 /std:c++latest로 변경합니다.
모듈 사용 - 실험적 도 선택합니다.
2. Visual Studio Installer 에서 자신이 사용하는 Visual Studio 2019에 대해 수정을 선택하고,
v142 빌드 도구용 C++ 모듈(x64/x86 - 실험적) 을 설치합니다.
기본적인 모든 모듈의 확장자는 .ixx입니다. (물론, 다른 확장자를 사용하여도 무방합니다.)
간단하게 "Hello, Module"을 출력하는 모듈을 생성하겠습니다.
Solution-> Project -> 소스 파일에서 추가 -> 모듈을 선택한 후, "speech"라는 모듈을 생성합니다.
다음과 같이 작성합니다.
// speech.ixx
export module speech;
export const char* GetPharse(){
return "Hello, Module";
}
// main.cpp
import std.core;
import speech;
using namespace std;
int main(){
cout << GetPharse() << endl;
}
매우 간단한 예입니다. speech라는 모듈이 있습니다.
main.cpp의 import를 통해 speech내의 정의된 단일 함수 'GetPharse'를 사용합니다.
export는 모듈이 다른 소스 파일에서 보일 수 있도록 하는 것입니다.
import는 export된 모듈이 단지 '번역 단위'에 의해 내 소스 파일에서 보일 수 있도록 하는 것입니다. ( 사실 이게 끝입니다. )
2. Module Names
모듈은 적절한 이름의 module-name으로 식별됩니다. 다음은 module-name의 문법입니다.
module-name:
[module-name-qualifier] identifier;
module-name-qualifier:
identifier "." |
module-name-qualifier identifier ".";
이는 모듈의 이름이 "." 으로 결합되어야하고, 0이 아닌 이름을 써야 함을 의미합니다.
이런 규칙을 제외하곤 언어의 나머지 부분과 동일합니다.
"."의 의미는 따로 없습니다. 그저 개발자들을 위해서 입니다 ( 논리적 계층 구조를 잘 따르기 위해 )
boost.asio.async_completion 이라는 모듈 이름은, 논리적 계층 구조를 이해하기 쉽습니다. 하지만 boost_asio_async_completion 이라는 이름과 스타일상의 차이만 있을 뿐, 차이는 없습니다. |
3. A New C++ Source Entity
이전에는, C++ 소스를 캡슐화하는 Translation Unit이 있었습니다. ( header와 cpp를 분리하는 것 )
C++ Module은 Module Unit이라는 새로운 유형의 Translation Unit을 도입합니다. 정의는 간단합니다.
Module Unit은 'module-declaration'이 포함된 translation unit 이다. |
'module-dclaration'은 이미 예로 보았습니다. 문법은 간단합니다.
module-declaration:
["export"] "module" module-name [module-partition] [attribute-specifier-seq] ";" ;
module-partition:
":" module-name;
기본적으로, 최상위 레벨에 'Module line'이 포함된 모든 파일은 'Module Unit' 입니다.(module-partition 은 다음에 다룹니다.)
중요 : Module Unit에는 여러 유형이 있으며, 각각의 의미를 이해하는것이 중요합니다.
- 'Module Interface Unit'은 module-declaration에 export 키워드가 포함된 모든 Module Unit 입니다.
- 이러한 Module들은 얼마든지 있을 수 있습니다.
- 'Module Implementation Unit'은 'Module Interface Unit'이 아닌 모든 Module Unit 입니다.
- (즉, module-declaration 에 export 키워드가 없습니다.)
- 'Module Partition'은 module-declaration에 'module-partition component'가 포함된 Module Unit 입니다.
- 'Module Interface Partition'은 'Module Interface Unit'과 'Module Partition'을 합친 것입니다.
- (즉, module-partition component과 export 키워드가 합쳐진 것 입니다.)
- 'Module Implementation Partition'은 'Module Implementation'과 'Module Partition'을 합친 것입니다.
- (즉, export 키워드가 없이 module-partition component만 포함한 경우 입니다.)
- 'Primary Module Interface Unit'은 '비-파티션 모듈'(non-partition module)이며, 'Module Interface Unit' 이기도 합니다.
위에 작성되지 않았지만, 'partition'이 아닌 Module implementation unit이 있을 수 있습니다.
4. Module Partitions
이 부분은 아직 Microsoft C++ 컴파일러에서 구현되지 않았습니다.
C++ Header와 마찬가지로, module을 여러 파일로 분할하고 세분화 할 필요가 없습니다.
그럼에도 불구하고 큰 소스 파일은 다루기 어려워 질 수 있으므로 C++ Module은 단일 Module을 함께 병합하여
전체 Module을 형성하는 별개의 Translation Unit으로 세분화 하는 방법도 있습니다.
이러한 세분화 방법을 'Partition' 이라고 합니다.
예를 들어, 같은 모듈에 포함하고 싶지 않은 두 개의 거대하고 번거로운 함수가 있다고 가정합시다.
export module speech;
export const char* GetPhraseEN() {
return "Hello, Module!";
}
export const char* GetPhraseKR() {
return "안녕, 모듈!";
}
이를 파티션을 이용하여 세분화 합시다.
// speech_english.cpp
export module speech:english;
export const char* GetPhraseEN() {
return "Hello, Module!";
}
// speech_korean.cpp
export module speech:korean;
export const char* GetPhraseKR() {
return "안녕, 모듈!";
}
Primary Interface Unit은 다음과 같이 모듈의 모든 인터페이스 파티션 파일을 가져오고(import), 다시 내보내야(export) 합니다.
// speech.ixx
export module speech;
export import : english;
export import : korean;
- 우리는 한개의 모듈 이름 'speech'를 가지고 있습니다.
- 'speech'는 2개의 partition인 'english'와 'korean'을 가지고 있습니다.
- export module <module-name>:<part-name>은 주어진<module-name>에 속하는 module interface partition임을 선언합니다.
- import : <part-name>은 naming된 partition을 가져옵니다. <part-name>인것은 모듈 'speech'에 속해야 합니다.
- export import : <part-name>은 결국, naming된 partition을 가져온 후, 그것을 다시 module interface의 일부로 표시하는 것입니다.
이것을 해석하면 다음과 같습니다.
'speech_english.cpp'와 'speech_korean.cpp'는 'module interface partition' 입니다.
'speech.ixx' 는 primary module interface unit 입니다.
module의 primary interface unit은 반드시 export import : <part-name>을 통해 모든 partition들을 export 해야합니다. 그렇지 않으면, 프로그램은 에러입니다. |
5. "Submodules' are not a Thing (Technically)
4번 예제를 세분화 할 수 있는 또 다른 방법이 있습니다.
// speech.ixx
export module speech;
export import speech.english;
export import speech.korean;
// speech_english.cpp
export module speech.english;
export const char* GetPhraseEN(){
return "Hello, Module!";
|
// speech_korean.cpp
export module speech.korean;
export const char* GetPhraseKR(){
return "안녕, 모듈!";
}
":" 대신 "."으로 나타냈습니다.
이 방법은 다른 언어의 모듈 디자인을 잘 알고있는 경우에는 위험이 될 수 있습니다.
// speech.ixx
export module speech;
// NOT OK:
export import .english;
export import .korean;
Python 사용자들은 '정규화 된 상대적 가져오기(?)'와 같은 구문에 익숙할 수 있습니다.
여기서 앞의 "."은 모듈 메커니즘이 주어진 이름을 가진 형제 모듈을 찾도록 지시합니다.
C++ module은 이러한 hierarchy 구조가 아니기 때문에 작동하지 않습니다.
speech.english와 speech.korean은 그저 speech의 서브 모듈일 뿐입니다. 완전히 분리되어 있습니다.
partitions을 사용할 때, interface partition의 모든 entity는 동일한 모듈의 일부입니다.
(위의 speech와 speech_xxx 와의 관계)
즉, entitiy를 소유하는 모듈은 해당 entitiy의 ABI의 일부가 됩니다.
-> 한 모듈에서, 다른 모듈로 entitiy를 이동하면 ABI가 중단될 수 있습니다.
'서브 모듈'을 사용하면, 사용자가 가져오는 내용을 보다 세분화 할 수 있습니다.
import boost; 같이 boost 전체를 가져오는것은 컴파일 타임에 치명적일 수 있습니다.
대신 speech.korean 처럼 원하는 서브 모듈만을 가져올 수 있습니다.
6. Module Implementation Units
지금까지는 'module interface unit'을 살펴았지만, 'module implementation unit'이 있습니다.
'module implementation unit'은 export 키워드가 없다고 위에서 설명했습니다.
implementation unit은 명명된 모듈에 속하게 됩니다.
이 항목들은 해당 항목이 속한 모듈에만 표시 됩니다.
이는 세부 사항을 숨기는 것이 가능하고, 수정이 downstream module에 영향을 미치지 않을 수 있으므로 빌드를 가속화 하는데 도움이 될 수 있습니다.
-> 아직 MSVC에서 어떻게 구현할지가 정해지지 않아서 향후 게시물에 작성합니다.
implementation unit을 사용하는 예제는 다음과 같습니다.
// speech.ixx
export module speech;
import : english;
import : korean;
export const char* GetPhraseEN();
export const char* GetPhraseKR();
// speech_english.cpp
module speech:english;
const char* GetPhraseEN(){
return "Hello, Module!";
}
// speech_korean.cpp
module speech:korean;
const char* GetPhraseKR(){
return "안녕, 모듈!";
}
이전과 비슷해 보이지만 몇 가지 변경사항이 있습니다.
- primary interface unit의 import에 더 이상 export 키워드가 없습니다.
- partition의 선언에도 export 키워드가 없습니다. 이것은 partition들을 implementation partition으로 만듭니다.
- 함수는 export 키워드 없이 implementation unit에서 정의됩니다. export와 관계가 없습니다.
- 함수는 export 키워드를 사용하여 primary interface unit에서 선언됩니다. 이렇게 하면 정의되지 않은 경우에도 함수가 module interface의 일부가 됩니다.
이는 결국 두 개의 함수를 선언하는 헤더 파일과 해당 함수를 정의하는 두 개의 소스 파일을 갖는 것과 유사합니다.
implementation unit을 변경해도 primary interface에 영향을 주지 않습니다.
즉 implementation unit을 수정하여도 primary interface에 영향을 주지 않으므로, 빌드 시간이 절약됩니다!
7. Restrictions on [export] {import, module} and Piartitions
'export import' 라는 구문은 처음에는 이상하게 보일 수 있지만, 의미가 있습니다.
주어진 모듈들을 다 가져온 다음, export를 하여 downstream importer들도 동일한 모듈을 import 할 수 있도록 하는 것입니다.
이런 정의된 방식때문에, module partition을 export하고 import 하는 방법에 대한 몇 가지 제한이 있습니다.
1) export import 는 interface partition에만 허용됩니다.
다음은 허용되지 않습니다.
module A:Foo // implementation unit 입니다.
export module A;
export import : Foo; // NO!, :Foo는 interface unit이 아닙니다.
A:Foo는 implementation 이기 때문에, A:Foo를 importer한테 export하는 것은 의미가 없습니다.
2) export import 는 한 모듈 당, 한 번만 표시되어야 합니다.
우리가 Cats라는 모듈을 정의한다고 가정합시다. 이를 정의하려면 하나 이상의 module interface unit이 필요합니다.
export module Cats;
export void meow();
export void purr();
여기서 Cats를 확장하기 위해, 다른 모듈 단위를 추가하게 되면 어떻게 될까요?
export module Cats;
export void hiss();
허용되지 않습니다. 파티션 이름이 없는 primary interface unit은 반드시 하나만 존재해야 합니다.
현재 컴파일러의 특성은 이러한 디자인에 대처할 수 없습니다.
컴파일러는 두 파일을 병합시킬 수 있지만, 두 파일이 상호 작용하는 방법에 대한 문제점이 있습니다.
컴파일러는 이 파일들이 '병합'되어야 하는지, 아니면 정의를 다른 파일로 옮겨야 하는지를 알 수 없습니다.
아마도 다른 질문이 제기될 수 있습니다 :
"사용자가 다른 Cats 파일을 정의하여 다른 사람의 모듈에 항목을 추가하는 것을 막는 이유는 무엇입니까?
3) export 는 implementation unit에서는 허용되지 않습니다.
당연합니다. implementation unit이 export를 하는 것은 의미가 전혀 없습니다.
4) 모든 interface partition들은 primary interface unit에서 다시 export 해야 합니다.
interface partition은 모듈의 인터페이스를 확장할 수 있기 때문에 (즉, primary interface unit 에서 선언되지 않은 entity를 추가)
컴파일러가 primary interface unit을 컴파일 하는 것만으로도 모듈의 인터페이스 전체를 볼 수 있어야 합니다.
export module Cats;
export import : Sounds;
export module Cats:Sounds;
export void meow();
export void hiss();
export module Cats:Behaviors;
export void eat();
export void sleep();
// importer.cpp
import Cats;
void foo(){
meow(); // OK
hiss(); // OK
eat(); // ERROR
}
당연하게도, primary interface unit인 Cats는 Behaviors를 export 하지 않았습니다.
위 프로그램은 잘못되어있습니다.
5) Module implementation unit은 강력합니다.
partition없이 별도의 파일에 module의 interface 와 implementation을 정의할 수 있습니다.
직관적이지 않을 수 있지만, 검사시 완벽하게 합리적입니다.
export module Cats;
export void dream();
export void sleep();
// cats_sleep.cpp
module Cats;
import sleep_info;
void sleep(){
if( is_rem_sleep()){
dream();
}
}
위의 cats_sleep.cpp는 Cats의 implementation unit입니다. dream이라는 정보를 가져오지 않았습니다.
Cats에는 모듈을 가져와 사용할 수 있는 충분한 정보가 포함되어 있습니다. 링커가 모든 정의를 해결합니다.
dream을 어디서 가져오지 않습니다. implementation unit은 자신이 속한 모듈을 암시적으로 가져옵니다.
물론 implementation partition을 만드는대신, anonymous implementation unit을 만들수도 있습니다.
(PIMPL을 참조하세요. C++ 모듈에서 PIMPL을 사용하는데 좋은 이유가 있습니다.)
module Gadgets:PrivWidget;
struct PrivWidge{
// ...
};
export module Gadgets;
import : PrivWidget;
export class Widget{
std::unique_ptr<PrivWidget> _data;
// ...
};
export Widget GetWidget() {
auto priv { std::make_unique<PrivWidget>(1, 2, 3) };
return Widget{ std::move(priv) };
}
'C++ > Modern' 카테고리의 다른 글
C++20) Ranges - 2 (0) | 2021.07.19 |
---|---|
C++20) Ranges - 1 (0) | 2021.06.15 |
C++20) Modules ( 모듈 ) - 1 (0) | 2021.03.14 |
C++20) Concepts ( 콘셉트, 개념 ) - 4 (0) | 2020.11.29 |
C++20) Concepts ( 콘셉트, 개념 ) - 3 (0) | 2020.11.28 |