HaruGakka Translate-Develop Blog

C++ 20 모듈 시스템

|

히스토리

2019-07-28 / 문서 생성.

2019-07-28 / 모듈 선언 방식 추가, 컴파일 불가능 한 코드들 수정. 모듈 시스템에서 불가능한 대표적인 것들, 서브 모듈이 안되는 이유, 문단 나누기 수정.

2019-07-28 / 파일 이름 중복 수정

C++ 20 모듈 시스템

C++20.. 에 수많은 것들이 추가되는데. 아마 적용되자마자 가장 빠르게 코드가 난독화 될(…) 모듈 시스템을 초간단 요약해봤습니다.

이제 모듈 시스템으로 헤더 <-> 소스 파일 구분이 불필요해졌습니다. 물론. 기존의 include도 지원합니다! 즉, 둘 다 알아야 할 수도 있습니다! ;ㅁ;…

기본적인 선언 방식

모듈을 정의할때는 기본적으론 다음과 같은 문법을 따릅니다.

모듈의-정의:
	["export"] "module" 모듈-이름 [모듈-파티션] ";"

모듈-파티션: 
	":" 모듈-파티션-이름;

이를 한번 보시고. 이제 한번 밑에 글을 천천히 읽어주세요!

기초 모듈 시스템

우선 예제코드를 보시죠 :

//module.cpp
exprot module hello;

export const char * get_hello() { return "Hello!, World!";  }
const char * get_not_hello() { return "Hello!, World!"; }

이제 이 파일을 들고올때는 간단히 아래와 같이 적으면 됩니다.

//main.cpp
import hello; 
import <iostream>; //기존 헤더파일도 이렇게 들고 올 수 있습니다. 세미클론은 필수 입니다.  

int main ()
{
	get_hello(); //컴파일 성공! 
	//get_not_hello(); //컴파일 실패..
}

바깥으로 들고오려면, 앞에 export를 선언해주면 됩니다.

파티션

모듈 시스템의 기능중 하나는 모듈을 파티션별로 나눌 수 있습니다. 한 모듈안에 여러개의 파일이 포함될 수 있습니다!

//module.cpp
export module hello; 
//이 선언은 별개의 모듈을 선언한게 아니라
//이 모듈의 부분(파티션)들을 선언한겁니다. 	

//둘의 차이점은, 별개의 모듈(아래 설명할 서브모듈)은 분리가 가능하지만. 
//모듈의 부분은 (파티션)은 분리가 불가능합니다. 
export import :eng;
export import :ko;
//중요한 점은, *반드시* 모든 파티션들을 선언해줘야 합니다. 직접적으로든, 간접적으로든. 이는 C++ 표준상 제약이며, 미정의 행동을 유발할 수 있습니다.
//module_eng.cpp
//모듈 분리! 이건 모듈의 부분입니다. 
export module hello:eng;  //이런 선언을 파티션을 인터페이스화 했다고 할 수 있습니다. 이제부터 외부노출이 가능합니다. 

export const char * get_hello_eng()
{ return "Hello, World!";}

//module_ko.cpp
//모듈 분리! 이건 모듈의 부분입니다. 
export module hello:ko;  //이런 선언을 파티션을 인터페이스화 했다고 할 수 있습니다. 이제부터 외부노출이 가능합니다. 

export const char * get_hello_ko()
{ return "안녕, 세상!";}
//main.cpp
import hello; 
import <iostream>

int main()
{
	std::cout << get_hello_eng() << get_hello_ko(); //정상 작동! 
}

구현과 정의단 분리

모듈 시스템에서도 구현단과 정의단의 분리가 가능합니다.

//module.cpp
//모듈 구현 분리! 여기서 선언만 해줍니다.
export module hello;
import :ko; //여기서 정의하면, export할 필요가 없습니다. 

export const char * get_hello_ko(); //선언! 외부 노출용 인터페이스이기 때문에, 선언부에서 export해줘야 합니다.
//module_ko.cpp
//모듈 분리! 이건 모듈의 부분입니다. 
module hello:ko; //역시 export해주지 않아도 됩니다. 

const char * get_hello_ko() //정의!
{ return "안녕, 세상!";}

으음. 저는 모듈의 일부만 불러오고 싶어요!

불가능합니다! ;ㅁ; 아래 예제를 봐주시죠.

//module_hello.cpp
export module hello;

export import hello.hi;
export import hello.welcome;

//module_hello_hi.cpp
export module hello.hi;

export const char * get_hello_ko() //정의!
{ return "안녕, 세상!";}

//module_hello_welcome.cpp
export module hello.welcome;

export const char * get_hello_eng()
{ return "Hello, World!";}
//main.cpp
import hello;

int main()
{
	get_hello_eng();
	get_hello_ko(); //둘다.. 되긴 합니다. 
}

위 예제를 보시면 문법상으로 C#의 System.Text와 같은 서브모듈의 분리가 가능한 것 ‘처럼’ 보이지만, 실제 구현상으론 그냥 별개의 모듈로 작동합니다. module에서 보다싶이. 그냥 다른 모듈을 import한 걸 외부에 노출시키겠다는 선언 그 이상도, 그 이하도 아닙니다. 헤더파일을 예로 들면.

#include <hello.h>
#include <hello_hi.h>
#include <hello_welcome.h>

이게 구현되지 않은 이유는 C++의 언어 특징에 밀접하게 관련이 있습니다 .

C++은 네이티브 언어이기에, API뿐만 아니라 ABI도 신경써야 합니다. 예를 들어 System과, System의 서브모듈(이고 싶은) System.Sub, System.Main이 있다고 해봅시다.

만약 C++이 다른 언어와 같이 서브모듈을 지원하려면 System.Sub를 가져올 시 연관 관계에 있는 System도 들고와줘야 합니다. 그야 System의 일부분이니까요. 그리고 사용자가 System.RemoveSystem을 만들어서 System에 붙일 수도 있겠지요. 하지만 모듈 시스템에선 불가능합니다. ABI를 덧 붙이는 방법은 없으니까요. 그래서 위 코드는 그냥 모듈 세 개의 이름이 다른 것 뿐입니다.

다만 이름이 ‘System’, ‘System.Submodule’인 건 여전히 가능합니다. 기술적으로는 아무런 의미가 없고, 바이너리도 독립적으로 나오겠지만 구분상으로 도움이 될 수 있습니다.

대표 오류 사례들.

외부 노출되지 않은 파티션은, 모듈이 export import할 수 없습니다.

//foo.cpp
module A:Foo;
//A.cpp
export module A; 
exprot import :Foo; // 불가능합니다! foo는 export되지 않았거든요.

위 오류는 파티션이 노출이 되지 않았기에 export도 불가능한 사례입니다. export import는 오직 export module이 된 파티션에만 적용됩니다. 즉. 이게 외부로 노출된 인터페이스( export module로 노출시키는 거지요!)가 아니라면 export import도 불가능합니다.

export module은 반드시 한번씩만 사용되야 합니다.

//A.cpp
export module A;
export void A2();
export void A3();
//A_anoter.cpp
export module A; 
export void A1();

export module은 여러번 나올 수 없습니다. 이유가 몇개 있는데요. 하나는 ‘ 다른 사람의 모듈에 강제로 자기 코드를 넣게 해도 되는가? ‘ 라는 이유가 있고요. 가장 중요한 이유는 현재 컴파일러는 이 설계에 대처하지 않았기 때문입니다.

조금 더 예시를 들어보면. 현재 파일이 두 개니까, 두 번의 컴파일러 콜이 생성되겠군요. 그런데 컴파일러 호출마다, 이 파일이 merge해야 하는지, 아님 그냥 사용자가 실수로 적은 건지 판단하기 어렵습니다. (게다가, 멀티 스레딩까지 가세한다고 생각해보면 머리가 터지겠죠.) 그냥 단순히 복붙을 했을수도 있고. ( 그러면 중복 인터페이스로 컴파일 에러를 내뱉을거고요. ) 사용자가 이 모듈에 코드를 집어 넣고 싶은 걸 수도 있고. ( 이럴 경우 머지를 해줘야 겠고요. ) 리펙토링하고 까먹어서 나온 걸지도 모르겠지요. 그걸 다 판단하기보다는 그냥 export module이 하나 이상이면 에러인게 더 간단하겠지요.

물론 컴파일러가 이 파일들을 merge 해줄 수는 있겠지만, 그로 인해 얻는 장점은 별로 없고. 컴파일러 설계만 더 복잡해 질 것 입니다.

모든 인터페이스 파티션들은 ( export module한 파티션들은 ) 반드시 기반 인터페이스( 파티션을 잡은 부모 모듈 )에서 다시 export 되야 합니다.

이름부터 어렵지만. 그냥 조심 해주시면 되는 간단한 문제입니다.

//module_cats_sound.cpp
export module Cats:Sounds;

export void yaong();
export void maww();

//module_cats_behavior.cpp
export module Cats:Behaviors;

export void export_object_to_ground();
export void eject_from_home(); 
export void kill_sopa();
//module_cats.cpp;

export module Cats;

export import :Behaviors;

import Cats;

int main() {
	export_object_to_ground();
	eject_from_home();
	kill_sopa(); 

	yaong(); //에러! export되지 않았습니다! 
}

파티션들을 외부에서 사용하고 싶으시면, 두가지만 조심해주시면 됩니다. 하나는 파티션을 인터페이스 유닛으로 뽑아주시고요. ( export module module:partion; ) 하나는 파티션을 친 기본 모듈에서 파티션을 export해주는 것입니다. ( exprot import :partion;)

참고 : Understanding C++ Modules: Part 1: Hello Modules, and Module Units

위 포스터의 영향을 강력히 받았습니다. 자세한 사항은 저 블로그를 참고해주세요!

History:

#Development/R&D #Development/C++/C++20

Comments