Module의 테스트 환경

concept를 지원하는 컴파일러 버전을 확인하시려면 다음의 경로에서 확인 할 수 있습니다

https://en.cppreference.com/w/cpp/compiler_support/20

module 컴파일러 지원 버전

저의 경우 MSVC 2019 community 16.11.15 버전이고 컴파일러 버전은 19.29을 사용하였습니다.

사용한 컴파일러 버전

또한 프로젝트 우 클릭 -> 구성 속성 -> 일반 -> c++ 언어 표준을 ISO C++ 20 표준으로 수정하였습니다.

C++ 컴파일러 버전 변경

모듈을 설명하기 전에 모듈이 도입되어야 했던 이유를 설명하기 위해서 기존의 c++ 빌드 과정과

문제점에 대해서 먼저 알아보겠습니다. 

기존의 C++ 빌드 과정

c++빌드 과정(https://st-lab.tistory.com/176)

C++ 프로그램의 빌드 과정은 전처리, 컴파일, 링크로 구성됩니다. 

전처리(Preprocessor)

전처리기는 소스 파일에 있는 #include나 #define 같은 지시문들을 처리합니다. 

#include 지시문을 헤더 파일로 치환하고 #define 지시문도 정의된 매크로로 치환합니다. 

#if, #else, #elif, #ifdef, #ifndef , #endif 같은 지시문을 기반으로 조건부 처리로 포함하거나 제외합니다. 

전처리기에서 생성된 구문을 번역 단위(translation unit) 라고 불리우고 이것을 컴파일러에 전달 합니다.

컴파일(compilation)

컴파일 과정에서는 전달 받은 번역 단위에 담긴 c++ 소스 코드를 해석해서 어셈블리 코드로 변환합니다. 

어셈블리 코드에 대응 되는 이진 파일을 출력하는데 이것을 목적(object) 파일이라고 합니다. 

목적 파일들은 목적 파일이 정의하지 않은 Symbol들도 참조 할 수 있습니다.

링크(linking)

목적(Object) 파일들을 링크해서 하나의 실행 파일이나 공유, 정적 라이브러리 파일을 만듭니다. 

여기서 목적 파일들의 Symbol을 검사해서 정상적인 참조인지 확인합니다. 

빌드 과정의 문제점

치환 문제

아래와 같이 선언된 간단한 프로그램이 있다고 생각해봅니다.

// main.cpp
int main()
{
    return 0;
}

// main1.cpp
#include <iostream>
int main()
{
    return 0;
}

cl이나 g++ 명령어에 옵션을 통해서 빌드과정에서 생성되는 번역단위를 확인 할 수 있습니다. 

# msvc의 경우
cl.exe /std:c++20 /c main.cpp /E > trans.log
cl.exe /std:c++20 /c main1.cpp /E > trans1.log

# gcc의 경우
g++ -E main.cpp > trans.log
g++ -E main1.cpp > trans1.log

생성된 번역단위 크기

간단한 프로그램을 동작하는데 trans1.log 크기가 큰 것을 확인 할 수 있습니다. 

#include <iostream>에 의한 치환 과정에서 iostream에서 참조하는 헤더들과 또 그 헤더들이 참조하는 헤더들이

중첩적인 과정을 통해서 번역 단위가 커지는 것을 확인 할 수 있습니다. 

전처리 매크로의 위험

매크로는 단순한 텍스트 치환으로 동작하는데 c++의 의미론과 무관하게 동작합니다. 

// common.h
#define SIZE	10

// item.h
#define SIZE	100

다음과 같이 같은 전처리 매크로 값에 충돌이 발생하고 cpp 파일에서 두 헤더를 참조한다고 할 때

순서를 어떻게 하느냐에 따라서 "SIZE" 값이 달라집니다. 

기호 중복 정의

c++에는 ODR이라는 용어가 있는데 One Definition Rule 입니다. 

번역 단위나 프로그램에서 함수의 선언은 하나이어야 합니다. 

c++에서는 해당 중복 정의를 회피하기 위해서 포함 가드(include guard)를 사용합니다.

// header.h
// #pragma once
#ifndef FUNC_H
#define FUNC_H

void func1() {

#endif

모듈의 장점

  • 모듈은 단 한번만 도입되며 비용이 거의 없습니다. 
  • 모듈을 도입하는 순서에 따른 차이가 없습니다. 
  • 모듈에서는 기호 중복 정의 문제가 거의 발생하지 않습니다. 
  • 모듈은 코드의 논리적 구조를 표현하는데 유리합니다.
    • 명시적으로 노출할 대상을 지정할 수 있습니다.
    • 다수의 모듈을 모아서 하나의 논리적 패키지로 제공할 수 있습니다. 
  • 소스 코드를 인터페이스 부분과 구현 부분으로 분리할 필요가 없습니다. 

모듈의 구조

컴파일러 별 모듈 파일 

컴파일러 별로 모듈에 대한 확장자를 다르게 설정 하였습니다. 

  • MSVC의 경우 ixx 확장자를 모듈로 사용합니다. 
  • Clang의 경우 cppm 확장자를 사용 했으나 최근에는 cpp를 사용합니다.
  • GCC의 경우 모듈 파일에 대해 특별한 확장자를 사용하지 않습니다. 

모듈의 구조

  • module로 시작하는 구문과 모듈의 선언 사이에는 전역 모듈 조각이라는 공간이 존재합니다. 
    • 전역 모듈 조각에는 아직 모듈화 되지 않은 헤더 파일을 추가 할 수 있습니다. 
  • 'export module xxx'은 모듈의 선언입니다. 여기서 부터 모듈이 시작됩니다.
    • 클라이언트는 선언된 모듈 이름을 통해서 모듈을 사용 할 수 있습니다.
  • 모듈안에서 참조할 모듈을 import를 통해서 가져올 수 있습니다.
  • 내보낼 모듈들은 export namespace로 선언된 공간에 선언합니다. 
  • 내보내지 않을 선언들은 export를 포함하지 않고 선언합니다. 
module; // 전역 모듈 조각

#include <string>		// 아직 모듈화 되지 않는 라이브러리 헤더

export module test;     // 모듈 선언, 여기서 부터 모듈 시작
import test2            // 사용할 모듈

// 내부에서만 사용할 선언들
const char* _getName() { return "test"; }

// 외부에 노출할 선언들
export namespace test {
	std::string name() { return std::string{ _getName() }; }
}

 

모듈 인터페이스 단위와 모듈 구현 단위

모듈이 커지면 모듈을 하나의 모듈 인터페이스 단위와 하나 이상의 모듈 구현 단위로 분할 하는 것을 권장합니다.

모듈 인터페이스

모듈 인터페이스에는 모듈 선언을 내보내는 선언이 있어야 합니다. ("export module math")

모듈이 내보낼 함수들을 정의합니다. 하나의 모듈에는 모듈 인터페이스는 하나이어야 합니다. 

module;

#include <string>

export module math;

export namespace math {
    int add(int fir, int sec);
    int sub(int fir, int sec);

    class Vec {
    public:
        std::string getName();
    };
}

모듈 구현 단위

모듈 구현 단위에도 모듈 선언이 있어야 하지만 export는 붙이지 않습니다. 

하나의 모듈에 여러개의 모듈 구현 단위가 있을 수 있습니다.

// mathImplementationUnit.cpp
module;
#include <numeric>
#include <string>

module math;

namespace math {
    int add(int fir, int sec) {
        return fir + sec;
    }

    int sub(int fir, int sec) {
        return fir - sec;
    }

    std::string Vec::getName() {
        return std::string{ "Vec" };
    }
}

cpp에서 모듈 사용 예제

// main.cpp
import math;
#include <iostream>


int main()
{
    math::Vec vec;
    auto a = vec.getName();
    std::cout << a << std::endl;
    return 0;
}

하위 모듈

모듈이 더욱 더 커지면 하위 모듈이나 모듈 분할이라는 방식으로 더 작은 단위로 관리 할수 있습니다.

아래 예제에서는 math를 math.math1, math.math2라는 하위 모듈로 나눠서 정의합니다. 

// mathModule.ixx
export module math;

export module math.math1;
export module math.math2;

// mathModule1.ixx
export module math.math1;
export int add(int fir, int sec) {
    return fir + sec;
}

// mathModule2.ixx
export module math.math1;
export int sub(int fir, int sec) {
        return fir - sec;
}

사용자는 다음과 같이 사용 할 수 있습니다. 

#include <iostream>
import math;

int main() {
    cout << '\n';
    cout << "add(3,4): " << add(3,4) << '\n';
    cout << "sub(3,4): " << sub(3,4) << '\n';
}

또는 직접 하위 모듈에 접근해서 사용 할 수도 있습니다.

// mathModuleClient1.cpp
// 하위 모듈을 직접 접근 할 수 있습니다. 
#include <iostream>
import math.math1;

int main() {
    cout << '\n';
    cout << "add(3,4): " << add(3,4) << '\n';
}

 

헤더 단위

헤더 단위(header unit)란 전통적인 헤더에서 모듈로 넘어가기 위한 방법입니다. 

단순히 #include 지시문에서 import 지시자로 바꾸고 세미콜론(;)을 붙이면 됩니다.

#include <iostream> => import <iostream>;

#include "MyHeader.h" => import "MyHeader.h";

이렇게 헤더 단위를 사용하면 컴파일러는 모듈에서 export 한것 처럼 처리하기 때문에 

기존의 #include 보다 빠릅니다. 

헤더 단위의 단점

모든 헤더를 헤더 단위로 도입할 수 없습니다. 

c++ 표준 라이브러리는 도입 가능하지만 C 헤더들은 도입 불가입니다. 

MSVC에서 STL 라이브러리를 헤더 단위로 사용해보기

msdn에서는 2가지 방법을 제시합니다ㅏ. 

  1. STL 라이브러리의 헤더 단위 정적 라이브러리 만들기
  2. STL 헤더를 검색하고 헤더 단위로 컴파일 하는 방법

여기서는 간단한 2번째 방법으로 진행해봅니다. 

프로젝트 옵션 설정

프로젝트 속성 페이지로 이동합니다. 

  1. 구성, 플랫폼을 모든 구성, 모든 플랫폼으로 수정합니다. 
  2. 구성 속성 -> C/C++ -> 일반의 아래 속성을 다음과 같이 변경합니다. 

설정 후에 다음과 같이 입력후 빌드를 실행합니다.

첫 실행시에 include 지시문으로 import 지시문으로 변환하는데 시간이 오래 걸립니다.

import math;
import <iostream>;


int main()
{
    std::cout << "import 빌드 완료" << std::endl;
    return 0;
}

참조

https://docs.microsoft.com/ko-kr/cpp/build/walkthrough-import-stl-header-units?view=msvc-160 

 

연습: STL 라이브러리를 헤더 단위로 가져오기

헤더 단위를 사용하여 Visual Studio에서 C++ STL(표준 템플릿 라이브러리) 라이브러리를 가져오는 방법을 알아봅니다.

docs.microsoft.com

 

'c++ > modern c++' 카테고리의 다른 글

[c++20] 3중 비교 연산자 (<=>)  (0) 2022.06.18
[c++20] consteval과 constinit  (0) 2022.06.16
[c++20] Concept  (0) 2022.06.10
[c++] condition variable(조건 변수)  (0) 2020.07.10
[c++] std::lock 관련 함수들  (0) 2020.05.22

+ Recent posts