모듈

모듈 (Modules)

헤더 파일을 #include로 끌어오다 보면 매크로 충돌이나 컴파일 시간 증가 같은 문제가 생기곤 해요. C++20의 모듈(modules)은 번역 유닛 간에 선언과 정의를 공유하는 새로운 방법으로, 헤더의 여러 사용처를 대체해요.

출처: cppreference

본문

대부분의 C++ 프로젝트는 여러 번역 유닛(translation unit)을 사용하기 때문에, 그 유닛들 사이에 선언과 정의를 공유해야 해요. 이 목적에 헤더가 두드러지게 사용되고, 예를 들어 표준 라이브러리는 해당 헤더를 포함함으로써 그 선언들을 제공할 수 있어요.

모듈은 번역 유닛 간에 선언과 정의를 공유하기 위한 언어 기능이에요. 헤더의 일부 사용처에 대한 대안이죠.

모듈은 네임스페이스와 직교(orthogonal)해요.

가장 간단한 모듈 예시부터 볼게요.

// helloworld.cpp
export module helloworld; // 모듈 선언

import <iostream>;        // import 선언

export void hello()       // export 선언
{
    std::cout << "Hello world!\n";
}
// main.cpp
import helloworld; // import 선언

int main()
{
    hello();
}

helloworld 모듈이 hello()를 내보내고(export), main.cpp가 그 모듈을 import해서 hello()를 호출해요. 헤더 없이 선언을 공유하는 방식이 보이죠.

문법

export(optional) module module-name module-partition(optional) attr(optional) ;   (1)
export declaration                                                               (2)
export { declaration-seq(optional) }                                              (3)
export(optional) import module-name attr(optional) ;                              (4)
export(optional) import module-partition attr(optional) ;                         (5)
export(optional) import header-name attr(optional) ;                              (6)
module;                                                                           (7)
module : private;                                                                 (8)

모듈 선언 (Module declarations)

번역 유닛은 모듈 선언을 가질 수 있으며, 그 경우 모듈 유닛(module unit)으로 간주돼요. 모듈 선언은 제공된다면 번역 유닛의 첫 번째 선언이어야 해요(나중에 다룰 전역 모듈 조각(global module fragment)은 예외). 각 모듈 유닛은 모듈 선언에 제공된 모듈 이름(그리고 선택적으로 파티션)과 연관돼요.

export(optional) module module-name module-partition(optional) attr(optional) ;

모듈 이름은 점으로 구분된 하나 이상의 식별자로 구성돼요(예: mymodule, mymodule.mysubmodule, mymodule2 ...). 점에는 내재된 의미가 없지만, 형식적으로 계층을 나타내는 데 사용돼요.

모듈 이름이나 모듈 파티션의 어떤 식별자가 객체형 매크로(object-like macro)로 정의되어 있으면, 프로그램은 ill-formed예요.

명명된 모듈(named module)은 같은 모듈 이름을 가진 모듈 유닛들의 모음이에요.

선언에 export 키워드가 있는 모듈 유닛을 모듈 인터페이스 유닛(module interface units)이라 하고, 그 외의 모든 모듈 유닛을 모듈 구현 유닛(module implementation units)이라고 불러요.

모든 명명된 모듈에 대해, 모듈 파티션을 지정하지 않는 모듈 인터페이스 유닛이 정확히 하나 있어야 해요. 이 모듈 유닛을 기본 모듈 인터페이스 유닛(primary module interface unit)이라고 불러요. 이것의 내보낸 내용은 해당 명명된 모듈을 import할 때 사용할 수 있게 돼요.

// (각 줄은 별도의 번역 유닛을 나타낸다)

export module A;   // 명명된 모듈 'A'의 기본 모듈 인터페이스 유닛 선언
module A;          // 명명된 모듈 'A'의 모듈 구현 유닛 선언
module A;          // 명명된 모듈 'A'의 또 다른 모듈 구현 유닛 선언
export module A.B; // 명명된 모듈 'A.B'의 기본 모듈 인터페이스 유닛 선언
module A.B;        // 명명된 모듈 'A.B'의 모듈 구현 유닛 선언

선언과 정의 내보내기 (Exporting declarations and definitions)

모듈 인터페이스 유닛은 선언(정의 포함)을 내보낼 수 있고, 다른 번역 유닛이 그것들을 import할 수 있어요. 선언을 내보내려면 export 키워드를 앞에 붙이거나, export 블록 안에 두면 돼요.

export declaration
export { declaration-seq(optional) }
export module A; // 명명된 모듈 'A'의 기본 모듈 인터페이스 유닛 선언

// hello()는 'A'를 import하는 번역 유닛에 보일 것이다
export char const* hello() { return "hello"; }

// world()는 보이지 않을 것이다.
char const* world() { return "world"; }

// one()과 zero()는 둘 다 보일 것이다.
export
{
    int one()  { return 1; }
    int zero() { return 0; }
}

// 네임스페이스 내보내기도 동작한다: hi::english()와 hi::french()가 보일 것이다
export namespace hi
{
    char const* english() { return "Hi!"; }
    char const* french()  { return "Salut!"; }
}

export를 붙인 선언만 다른 유닛에 보이고, 붙이지 않은 건 모듈 안에 갇힌다는 점을 확인해 볼게요. export 블록 안에 여러 선언을 모아서 내보낼 수도 있어요.

모듈과 헤더 유닛 import하기 (Importing modules and header units)

모듈은 import 선언(import declaration)을 통해 import돼요:

export(optional) import module-name attr(optional) ;

주어진 명명된 모듈의 모듈 인터페이스 유닛에서 export된 모든 선언과 정의는 import 선언을 사용하는 번역 유닛에서 사용할 수 있게 돼요.

import 선언은 모듈 인터페이스 유닛에서 내보낼 수 있어요. 즉 모듈 B가 A를 export-import하면, B를 import하는 것은 A의 모든 export도 보이게 해요.

모듈 유닛에서는 모든 import 선언(export-import 포함)이 모듈 선언 다음, 다른 모든 선언 앞에 그룹 지어져야 해요.

/////// A.cpp (명명된 모듈 'A'의 기본 모듈 인터페이스 유닛)
export module A;

export char const* hello() { return "hello"; }

/////// B.cpp (명명된 모듈 'B'의 기본 모듈 인터페이스 유닛)
export module B;

export import A;

export char const* world() { return "world"; }

/////// main.cpp (모듈 유닛이 아님)
#include <iostream>
import B;

int main()
{
    std::cout << hello() << ' ' << world() << '\n';
}

B가 A를 export-import하기 때문에, main.cpp가 B만 import해도 hello()를 쓸 수 있어요.

#include는 모듈 유닛에서(전역 모듈 조각 밖에서) 사용하면 안 돼요. 포함된 모든 선언과 정의가 모듈의 일부로 간주되기 때문이에요. 대신 헤더를 import 선언으로 헤더 유닛(header unit)으로서 import할 수도 있어요:

export(optional) import header-name attr(optional) ;

헤더 유닛은 헤더에서 합성된 별도의 번역 유닛이에요. 헤더 유닛을 import하면 그 모든 정의와 선언에 접근할 수 있게 돼요. 전처리기 매크로에도 접근할 수 있어요(import 선언은 전처리기가 인식하기 때문에).

하지만 #include와 달리, import 선언 시점에 이미 정의된 전처리 매크로는 헤더의 처리에 영향을 주지 않아요. 일부 경우에는 불편할 수 있는데(일부 헤더는 전처리 매크로를 설정의 한 형태로 사용), 그럴 때는 전역 모듈 조각을 사용해야 해요.

/////// A.cpp (명명된 모듈 'A'의 기본 모듈 인터페이스 유닛)
export module A;

import <iostream>;
export import <string_view>;

export void print(std::string_view message)
{
    std::cout << message << std::endl;
}

/////// main.cpp (모듈 유닛이 아님)
import A;

int main()
{
    std::string_view message = "Hello, world!";
    print(message);
}

전역 모듈 조각 (Global module fragment)

모듈 유닛은 전역 모듈 조각으로 접두될 수 있는데, 헤더 import가 불가능한 경우(특히 헤더가 전처리 매크로를 설정으로 사용할 때) 헤더를 포함하는 데 사용돼요.

module; preprocessing-directives(optional) module-declaration

모듈 유닛에 전역 모듈 조각이 있으면, 첫 선언은 module;이어야 해요. 그다음 전역 모듈 조각에는 전처리 지시자만 나타날 수 있어요. 그다음 표준 모듈 선언이 전역 모듈 조각의 끝과 모듈 내용의 시작을 표시해요.

/////// A.cpp (명명된 모듈 'A'의 기본 모듈 인터페이스 유닛)
module;

// _POSIX_C_SOURCE를 정의하면 POSIX 표준에 따라
// 표준 헤더에 함수들이 추가된다.
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>

export module A;

import <ctime>;

// 오직 데모용(나쁜 난수 소스)이다.
// 대신 C++ <random>을 사용하라.
export double weak_random()
{
    std::timespec ts;
    std::timespec_get(&ts, TIME_UTC); // from <ctime>

    // POSIX 표준에 따라 <stdlib.h>에 제공된다.
    srand48(ts.tv_nsec);

    // drand48()은 0과 1 사이의 난수를 반환한다.
    return drand48();
}

/////// main.cpp (모듈 유닛이 아님)
import <iostream>;
import A;

int main()
{
    std::cout << "Random value between 0 and 1: " << weak_random() << '\n';
}

module;로 시작해서 매크로를 정의하고 헤더를 포함한 뒤, export module A;로 실제 모듈 내용을 시작하는 구조예요.

비공개 모듈 조각 (Private module fragment)

기본 모듈 인터페이스 유닛은 비공개 모듈 조각(private module fragment)으로 접미될 수 있는데, 이걸 쓰면 모듈을 단일 번역 유닛으로 표현하면서도 모듈의 모든 내용을 importer가 접근할 수 있게 만들지 않을 수 있어요.

module : private; declaration-seq(optional)

비공개 모듈 조각은 다른 번역 유닛의 동작에 영향을 줄 수 있는 모듈 인터페이스 유닛의 부분을 끝내요. 모듈 유닛이 비공개 모듈 조각을 포함하면, 그것이 자기 모듈의 유일한 모듈 유닛이 돼요.

export module foo;

export int f();

module : private; // 다른 번역 유닛의 동작에 영향을 줄 수 있는
                  // 모듈 인터페이스 유닛의 부분을 끝낸다
                  // 비공개 모듈 조각을 시작한다

int f()           // foo의 importer에게 도달할 수 없는 정의
{
    return 42;
}

f()의 정의가 비공개 모듈 조각 안에 있어서, importer는 f()가 무엇을 하는지 몰라도 export int f(); 선언만으로 호출할 수 있어요.

모듈 파티션 (Module partitions)

모듈은 모듈 파티션 유닛(module partition units)을 가질 수 있어요. 이것은 모듈 선언이 모듈 파티션을 포함하는 모듈 유닛으로, 파티션은 콜론 :으로 시작하고 모듈 이름 뒤에 놓여요.

export module A:B; // 모듈 'A'의 모듈 인터페이스 유닛, 파티션 ':B'를 선언한다.

모듈 파티션은 정확히 하나의 모듈 유닛을 나타내요(두 모듈 유닛이 같은 모듈 파티션을 지정할 수 없음). 그것들은 명명된 모듈 안에서만 보여요(명명된 모듈 밖의 번역 유닛은 모듈 파티션을 직접 import할 수 없음).

모듈 파티션은 같은 명명된 모듈의 모듈 유닛이 import할 수 있어요.

export(optional) import module-partition attr(optional) ;
/////// A-B.cpp
export module A:B;
...

/////// A-C.cpp
module A:C;
...

/////// A.cpp
export module A;

import :C;
export import :B;

...

모듈 파티션의 모든 정의와 선언은, export 여부와 무관하게, import하는 모듈 유닛에 보여요.

모듈 파티션은 모듈 인터페이스 유닛일 수 있어요(모듈 선언에 export가 있을 때). 그것들은 기본 모듈 인터페이스 유닛이 export-import해야 하고, 모듈이 import될 때 그 export 문장들이 보여요.

///////  A.cpp
export module A;     // 기본 모듈 인터페이스 유닛

export import :B;    // 'A'를 import할 때 Hello()가 보인다.
import :C;           // WorldImpl()이 이제 'A.cpp'에만 보인다.
// export import :C; // ERROR: 모듈 구현 유닛을 export할 수 없음.

// World()는 'A'를 import하는 어떤 번역 유닛에도 보인다.
export char const* World()
{
    return WorldImpl();
}
/////// A-B.cpp
export module A:B; // 파티션 모듈 인터페이스 유닛

// Hello()는 'A'를 import하는 어떤 번역 유닛에도 보인다.
export char const* Hello() { return "Hello"; }
/////// A-C.cpp
module A:C; // 파티션 모듈 구현 유닛

// WorldImpl()은 ':C'를 import하는 'A'의 어떤 모듈 유닛에도 보인다.
char const* WorldImpl() { return "World"; }
/////// main.cpp
import A;
import <iostream>;

int main()
{
    std::cout << Hello() << ' ' << World() << '\n';
    // WorldImpl(); // ERROR: WorldImpl()은 보이지 않음.
}

A:B는 인터페이스 파티션이고 A:C는 구현 파티션이에요. A.cpp:B를 export-import해서 Hello()를 밖으로 내보내고, :C는 내부에서만 써서 WorldImpl()은 모듈 안에 숨어요.

모듈 소유권 (Module ownership)

일반적으로, 모듈 유닛에서 모듈 선언 뒤에 나타나는 선언은 그 모듈에 부착(attached)돼요.

엔티티의 선언이 명명된 모듈에 부착되면, 그 엔티티는 그 모듈에서만 정의될 수 있어요. 그런 엔티티의 모든 선언은 같은 모듈에 부착되어야 해요.

선언이 명명된 모듈에 부착되고 export되지 않으면, 선언된 이름은 모듈 연결(module linkage)을 가져요.

export module lib_A;

int f() { return 0; } // f는 모듈 연결을 가짐
export int x = f();   // x는 0과 같음
export module lib_B;

int f() { return 1; } // OK, lib_A의 f와 lib_B의 f는 서로 다른 엔티티
export int y = f(); // y는 1과 같음

엔티티의 두 선언이 서로 다른 모듈에 부착되면 프로그램은 ill-formed예요. 둘 중 하나가 다른 쪽에 도달할 수 없으면 진단이 필요 없어요.

/////// decls.h
int f(); // #1, 전역 모듈에 부착
int g(); // #2, 전역 모듈에 부착
/////// M의 모듈 인터페이스
module;
#include "decls.h"
export module M;
export using ::f; // OK, 엔티티를 선언하지 않음, #1을 내보냄
int g();          // Error: #2와 일치하지만 M에 부착됨
export int h();   // #3
export int k();   // #4
/////// 다른 번역 유닛
import M;
static int h();   // Error: #3와 일치
int k();          // Error: #4와 일치

다음 선언들은 어떤 명명된 모듈에도 부착되지 않아요(따라서 선언된 엔티티는 모듈 밖에서 정의될 수 있음):

  • 외부 연결을 가진 네임스페이스 정의.
  • 언어 연결(language linkage) 사양 안의 선언.
export module lib_A;

namespace ns // ns는 lib_A에 부착되지 않음
{
    export extern "C++" int f(); // f는 lib_A에 부착되지 않음
           extern "C++" int g(); // g는 lib_A에 부착되지 않음
    export              int h(); // h는 lib_A에 부착됨
}
// ns::h는 lib_A에서 정의되어야 하지만, ns::f와 ns::g는
// 다른 곳(예: 전통적인 소스 파일)에서 정의될 수 있다.

참고 사항 (Notes)

기능 테스트 매크로 표준 기능
__cpp_modules 201907L C++20 모듈 — 코어 언어 지원
__cpp_lib_modules 202207L C++23 표준 라이브러리 모듈 stdstd.compat

키워드

private, module, import, export

결함 보고 (Defect reports)

DR 적용 대상 발표 당시 동작 올바른 동작
CWG 2732 C++20 import 가능한 헤더가 import 시점의 전처리기 상태에 반응할 수 있는지 불명확했음 반응 없음
P3034R1 C++20 모듈 이름과 모듈 파티션이 객체형 매크로로 정의된 식별자를 포함할 수 있었음 금지됨

외부 링크

  1. Chuanqi Xu, 2025 — C++20 Modules: Practical Insights, Status and TODOs
  2. Chuanqi Xu, 2025 — C++20 Modules: Best Practices from a User's Perspective

더 알아보기