본문으로 바로가기

C++20) Modules ( 모듈 ) - 2

category C++/Modern 2021. 3. 14. 21:23

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.cppimport를 통해 speech내의 정의된 단일 함수 'GetPharse'를 사용합니다.

 

export는 모듈이 다른 소스 파일에서 보일 수 있도록 하는 것입니다.

importexport된 모듈이 단지 '번역 단위'에 의해 내 소스 파일에서 보일 수 있도록 하는 것입니다. ( 사실 이게 끝입니다. )

 

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-declarationexport 키워드가 포함된 모든 Module Unit 입니다.
  • 이러한 Module들은 얼마든지 있을 수 있습니다.
  • 'Module Implementation Unit''Module Interface Unit'이 아닌 모든 Module Unit 입니다.
  • (즉, module-declarationexport 키워드가 없습니다.)
  • 'Module Partition'module-declaration'module-partition component'가 포함된 Module Unit 입니다.
  • 'Module Interface Partition''Module Interface Unit' 'Module Partition'을 합친 것입니다.
  • (즉, module-partition componentexport 키워드가 합쳐진 것 입니다.)
  • '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>namingpartition을 가져옵니다. <part-name>인것은 모듈 'speech'에 속해야 합니다.
  • export import : <part-name>은 결국, namingpartition을 가져온 후, 그것을 다시 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.englishspeech.korean은 그저 speech의 서브 모듈일 뿐입니다. 완전히 분리되어 있습니다.

 

partitions을 사용할 때, interface partition의 모든 entity는 동일한 모듈의 일부입니다.

(위의 speechspeech_xxx 와의 관계)

즉, entitiy를 소유하는 모듈은 해당 entitiyABI의 일부가 됩니다. 

-> 한 모듈에서, 다른 모듈로 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 unitimport에 더 이상 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 partitionexport하고 import 하는 방법에 대한 몇 가지 제한이 있습니다.

 

1) export import 는 interface partition에만 허용됩니다.

 

다음은 허용되지 않습니다.

module A:Foo // implementation unit 입니다.
export module A;

export import : Foo;	// NO!, :Foo는 interface unit이 아닙니다.

A:Fooimplementation 이기 때문에, A:Fooimporter한테 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 unitexport를 하는 것은 의미가 전혀 없습니다.

 

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 unitCatsBehaviorsexport 하지 않았습니다.

위 프로그램은 잘못되어있습니다. 

 

5) Module implementation unit은 강력합니다.

 

partition없이 별도의 파일에 moduleinterfaceimplementation을 정의할 수 있습니다.

직관적이지 않을 수 있지만, 검사시 완벽하게 합리적입니다.

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.cppCats 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