본문 바로가기
C++

[C++20] Module 적용해 보기 (with CMake)

by 매운돌 2024. 1. 1.

최근에 template meta programming를 공부 하면서 예제코드 repository를 Module로 구성해 보면 좋겠다는 생각을 하게되었습니다. Module에 대해서 예전에 간단하게 공부한적이 있었지만, 그때 Visual Studio 환경에서 예제를 잠깐 따라한 정도라서 전혀 기억이 나지 않았고, 현재 예제 코드가 CMake를 기반으로 구성되어 있기 때문에, 추가적으로 공부가 필요했습니다. 

 

Module을 적용하는 이유

Module의 장점을 이야기하기 전에 기존 방식(Header 파일을 이용한 방식)에 어떠한 단점들이 있는지 알아볼 필요가 있습니다. 기존에는 #include를 통해서 해더를 포함하게 되면, 전처리가 수행되는 동안 Header의 내용이 그대로 복사되게 됩니다. 따라서 Header의 내용이 재귀적으로 확장하면서 프로그램이 커지게 되고 같은 내용을 계속 반복적으로 컴파일하게 됩니다. 그리고 이는 컴파일 시간을 늘리는 문제점을 야기했습니다. (이를 해결하기 위해서 Header와 cpp파일을 분리 혹은Precompiled Header를 사용해서 불필요한 컴파일을 최소화 했습니다.)

또한 Header의 내용이 그대로 복사되기 때문에 코드가 중복되는 문제가 발생하고 이를 해결하기 위해서 Header에서는전방 선언을 하거나 아래와 같은  가드 메커니즘을 사용 했었습니다. 

#ifndef INCLUDE_GUARD_HEADER_SAMPLE
#define INCLUDE_GUARD_HEADER_SAMPLE

void f() {}
class A {};

#endif

 

그리고 Header를 포함하는 순서에 따라서 의존성이 깨지기도 하였습니다.

// hoge.hpp
int hoge(int num)
{
    int a = 10;
    return num + a;
}

// fuga.hpp
#define a 100
int fuga(int num)
{
    return num + a;
}

// main.cpp
#include "hoge.hpp"
#include "fuga.hpp"
#include <iostream>

int main()
{
    std::cout << hoge(1) << ", " << fuga(1) << std::endl;
}

위의 코드는 hoge.hpp 및 fuga.hpp를 순서대로 포함하면 main.cpp 컴파일이 통과됩니다. 그러나 fuga.hpp 및 hoge.hpp를 순서대로 포함하면 main.cpp 가 컴파일되지 않습니다.  앞서 이야기한 이유들 때문에 Header에서 간단한 수정도 쉽지 않은 경우가 종종 발생했고, 

 

 

모듈을 사용하면 전처리기를 사용하지 않고 프로그램을 분할할 수 있습니다. 즉, 모듈은 컴파일 타임에 종속성을 자동으로 해결해 줍니다. 따라서 Header를 사용하면서 발생하는 문제점들의 근본적인 해결책이 될 수 있습니다. 여기서 모듈의 사용법이나 적용 원리를 이야기 하기에는 맞지 않으니 아래의 글을 참고해 주시면 감사하겠습니다.

C++에서의 모듈 개요 | Microsoft Learn

 

C++에서의 모듈 개요

C++20의 모듈은 헤더 파일에 대한 최신 대안을 제공합니다.

learn.microsoft.com

 

 

Module 적용

모듈은 C++의 빌드의 근본적인 변화이기 때문에 C++ ecosystem(컴파일러, 빌드 시스템, 라이브러리 등등..)에 완벽히 적용 됬다고 볼 수 없습니다. 따라서 아래의 링크를 통해 현재 각 컴파일러, 빌드 시스템 별 진행 상황이 어떠한지 확인하고 자기 환경에 맞게 적용해야 합니다. 또한 아직 작업이 계속 진행중이기 때문에 지원이 된다고 하더라도 완벽하지 않아 보입니다. (아래 글도 23년12월 기준으로 4달 전에 업데이트 되었지만, 아직 최신의 정보를 노출하고 있지는 않는것 같습니다.)

royjacobson/modules-report (github.com)

 

GitHub - royjacobson/modules-report

Contribute to royjacobson/modules-report development by creating an account on GitHub.

github.com

 

우선 저의 환경은 아래와 같습니다.

빌드 환경: Windows 10(WSL Ubuntu 22.04.3 LTS)

컴파일러: clang version 17.0.6

CMake: 3.28.1

Ninja Generator: 1.11.1

CMake 3.28 Release 노트에서 위와 같이 Clang 16부터 지원한다고 했지만, Stack Overflow 글에서 16버전에 몇 가지 버그가 있다고 하여 최신 버전인 17.0.6버전을 다운 받았습니다. 그리고 기존에 Make를 통해 빌드를 했었지만, Module은Ninja 버전 1.11 이상만 지원한다고 하여 새롭게 설치하였습니다.
(CMake를 패키지 매니저를 통해 최신 버전을 설치하려고 했는데 동작하지 않아서 이 링크의 글을 참고하여 다운받아 설치하였습니다.)

 

현재 프로젝트 구성은 아래와 같습니다.
(hotstone1993/template: Template Meta Programming (github.com))

.
├── CMakeLists.txt
└── src
    ├── main.cpp
    └── math.ixx

 

이제 CMakeLists.txt 코드부터 확인하면 아래와 같습니다.

cmake_minimum_required(VERSION 3.28)

project(Template LANGUAGES CXX)

add_executable(${PROJECT_NAME})

set_target_properties(
  ${PROJECT_NAME}
  PROPERTIES CXX_STANDARD 20
             CXX_STANDARD_REQUIRED ON)

target_sources(${PROJECT_NAME} 
    PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp
)

target_sources(${PROJECT_NAME} 
    PUBLIC
    FILE_SET all_my_modules TYPE CXX_MODULES FILES
    ${CMAKE_CURRENT_SOURCE_DIR}/src/math.ixx
)

기존에 add_executable에 모든 소스 코드를 포함하게 구성했었는데 이제는 소스코드와 모듈쪽 코드를 target_sources로 구분하여 관리합니다.

 

모듈쪽 코드는 아래와 같습니다. 

module;

#include <type_traits>
#include <vector>

export module Math;

export auto sum(const auto& ... args) {
    using common_type = std::common_type_t<std::remove_cvref_t<decltype(args)>...>;

    std::vector v { static_cast<common_type>(args)...};
    using return_type = typename decltype(v)::value_type;
    return_type sum{};

    for (const auto& e: v) {
        sum += e;
    }

    return sum;
}

우선 #include를 사용하기 위해서 상단에 module이라 선언을 하고 아래에 header들을 포함했습니다. 

그리고 이 모듈을 외부에서 노출하기 위해 export를 붙여줬고, sum함수도 외부에서 사용할 수 있게 export를 붙여주었습니다.

header를 포함하지 않고, import std.core 모듈을 포함하게 해보려 했지만 계속해서 찾을 수 없다는 에러가 발생했습니다.

(아직 현재 clang 컴파일러 버전에서는 표준 라이브러리 지원이 안된는것 같습니다.)

 

아래는 module을 사용하는 쪽의 코드입니다. 

#include <iostream>

import Math;

int main() {
    std::cout << "sum - " << sum(1, 2, 3.1, 4, 5) << std::endl;
    return 0;
}

import로 module을 불러오고 우리가 사용하고자 했던 함수를 호출하여 사용할 수 있습니다.