[C++][ADAPT]データ分析、処理ライブラリADAPTの更新情報(4)。

ADAPTについての説明はこちらへ。
ADAPTのGitHubリポジトリはこちらへ。

文字列からラムダ関数へ変換する機能の実装

Rttiのラムダ関数に限定されるが、文字列をパースしてラムダ関数に変換するParse関数を用意した。Rttiは静的型情報が削除されているので、こんな芸当も可能だ。というより、こんな芸当を最終的に実現することが、わざわざC++でありながら動的型のラムダ関数なんてものを用意していた理由の一つである。ADAPTの機能を何らかのアプリケーションに組み込む際に、ラムダ関数を外部から文字列パラメータで与えられるようにすることが目的だ。

DTree t;
//...tへのデータ構造定義やデータ格納処理
using Lambda = eval::RttiFuncNode<DTree>;
Lambda lambda1 = Parse(t, "a + mean_if(hypot(b, c), d % 2 == 0)");
Lambda lambda2 = Parse(t, "a - a.at(pos2() - 1i64)");
//上記は以下と同等。
//ADAPT_GET_PLACEHOLDERS(t, a, b, c, d);
//Lambda lambda1 = a + mean_if(hypot(b, c), d % 2 == 0);
//Lambda lambda2 = a - a.at(t.pos2() - (int64_t)1);
Bpos bpos{ 1, 4, 3 };
double res = lambda(t, bpos).f64();

文字列式ラムダ関数の文法は通常の書き方とほとんど同じだが、少し異なるところがある。

  • フィールドへのアクセスは通常ならプレースホルダを用いるが、文字列式ラムダ関数の場合はコンテナクラスの構造定義時に与えた名前をそのまま指定する。
  • 連結コンテナ用は未実装。技術的に可能だが色々と調整が必要なので後回しにした。
  • ユーザー定義関数は現時点で非対応。
  • コンテナのメンバ関数として実装される関数(pos、size)は、メンバ関数としてではなく単なる一般関数のように呼び出す。
  • 整数、浮動小数点リテラルはi16、f32のような接尾辞を付与することで型を調整できる。デフォルトではi32、f64になる。

なお、本機能はラムダ関数の保つ機能のほとんどすべてが組み込まれている関係で、ビルドが非常に重く、吐き出されるバイナリが巨大である。そのためこれの使い方は下記の選択肢を用意している。

使い方1. ヘッダオンリーライブラリとして使う

<OpenADAPT/Parser_impl.h>をincludeするだけでよい。ただしビルドは極めて遅いし重い。Ryzen 7 7700X + メモリ32GBのPC上でMSVCを用いてビルドしたときは何とかぎりぎり通る程度の状態だった。GCCだと失敗するかもしれない。

使い方2. 事前にビルドする

OpenADAPTのビルド時、cmakeコマンドに-DENABLE_PREBUILT_PARSER=ONというオプションを与えることで、DTree/DTable/DHistのためのParse関数を事前にビルドすることができる。-DPREBUILT_PARSER_TARGET=DTree;DHistのように、必要なコンテナの種類を指定してもよい。
この方法を取った場合、多数のソースコードにコンパイル単位を分割した上でビルドするので、標準的なPCでも時間はかかるがビルド可能な程度になっているはずだ。
出力ファイルが中間生成物を合わせて数GBに達してしまうので、サイズを削減したい場合は-DBUILD_SHARED_LIBS=ONとして共有ライブラリ化することも検討されたい。

これを使用する場合は、<OpenADAPT/Parser.h>をincludeしつつ、各ビルド成果物へ静的/動的にリンクすればよい。もしMSVCを使用しており、かつ-DBUILD_SHARED_LIBS=ONにより共有ライブラリとしてビルドした場合、includeよりも前に#define ADAPT_DLL_IMPORTと定義しておくことを推奨する1

CMakeから使用する場合は以下のようにしても良い。

find_package(OpenADAPT REQUIRED)
target_link_libraries(myproj PRIVATE OpenADAPT::Parser)

関数名整理

ラムダ関数内で使用できるsizecountという階層関数であるが、名前と実際の挙動との対応を整理するためにそれぞれcountallcount_ifに変更した。従来の名前はしばらくdeprecatedとして使用可能にしておくが、文字列式ラムダ関数では新しい名前しか使えない。
本当はcountallではなくcountにしたかったが、元々のcount関数と被ってしまうのは非常にまずいのでこのようにしている。とはいえ、既に後述する新しいsize関数があるので出番は多くないだろう。

ラムダ関数内で使用するsizepos関数の追加

こちらのsize関数はコンテナクラスのメンバとして呼び出すsizeである。指定された要素に属している子要素数を返す。 これは殆どの場合、階層関数のcountall(上述のとおりsizeから名前を変更した)に同じ階層のフィールドを与えた場合と同等の結果を返す。

DTree t;
auto lambda1 = t.size(2_layer);//現在指し示している1層要素に所属する2層要素の数を返す。

ADAPT_GET_PLACEHOLDERS(t, fld_layer2);
assert(fld_layer2.GetLayer() == 2_layer);
auto lambda2 = countall(fld_layer2);//指定された1層要素に属す全2層要素のfld_layer2に対してアクセスを試み、アクセスに成功した数を返す。
//lambda1とlambda2は計算過程が異なるが、殆どの場合同じ結果を返す。

違いは速度と、アクセス不能な何かがある場合の挙動、そして連結コンテナで未実装である点だ。
* 階層関数のsize関数は律儀に一つ一つの要素で引数部分の計算を試みながら数えるので動作が遅い。こちらはいちいち数えず単にstd::vector::sizeのように範囲を計算するだけなので高速である。
* 階層関数のsize関数は、引数として与えられた部分の計算を試み、何らかの理由でアクセスに失敗したり計算結果が不正となったりした場合にはその要素をカウントしないというちょっと特殊な振る舞いがある2。新size関数は単にコンテナの要素数を参照して返すだけなのでアクセスという概念がなく、そのような計算を実行できない。
* 新size関数は連結コンテナでは使用できない。というのも、挙動が直感とは異なるものになりかねないため3

pos関数は現在位置を返す関数群である。

Bpos b{ 5, 10, 15 };
auto pos_layer0 = t.pos0();
auto pos_layer1 = t.pos1();
auto pos_layer2 = t.pos2();
//t.pos(N_layer)はt.posN()に等しい。
int64_t p0 = pos_layer0(t, b).i64(); // 5
int64_t p1 = pos_layer1(t, b).i64(); // 10
int64_t p2 = pos_layer2(t, b).i64(); // 15

ToVectorExtractなどのrange conversionによって一括操作する場合に現在位置を参照する手段が必要になることが往々にしてあったため、今更ながら設けた。

基本インデックス型をuint32_tからint64_tへ変更

もともとインデックス型はメモリ使用量削減のために歴史的に32bitとなっており、少しでも上限値を増やすためにunsignedにしていたのだが、ADAPTのフィールドにはunsigned型が存在しないという大きな矛盾点がありどうするか悩んでいた。最近は我々が扱うデータサイズも32bitの範囲を超えつつあることも踏まえ、今回、思い切って64bitに拡大することにした。

BinJointの追加

SHist/DHistの特定のビンに連結する方法。原理的にはKeyJointでも代用可能だが、Histの0層要素に連結する場合はこちらのほうが呼び出しがシンプルである。

//生徒の後期期末試験(exam == 3)の数学の点数をヒストグラム化
ADAPT_GET_PLACEHOLDERS(*m_tree, class_, number, name, exam, math, english);
auto tree = *m_tree | Filter(exam == 3) | ADAPT_EXTRACT(class_, number, name, math);
auto hist = *m_tree | Filter(exam == 3) | ADAPT_HIST(cast_f64(math).named("math"), 5., class_, number, name);
//treeの2層(試験成績層)とhistの0層(ヒストグラムのビン)を連結
auto jt = Join(tree, 2_layer, 0_layer, hist);
auto [class_r0, number_r0, name_r0, math_r0] = jt.GetPlaceholders<0>("class_", "number", "name", "math");
auto [class_r1, number_r1, name_r1, math_r1] = jt.GetPlaceholders<1>("class_", "number", "name", "math");
//連結対象にはtree側の数学成績が収まるであろうビンを指定
jt.SetBinJoint<1_rank>(cast_i32(math_r0 / 5.));
//数学の点数が一致する生徒の一覧を表示
jt | Filter(math_r0 == math_r1, !(name_r0 == name_r1 && number_r0 == number_r1)) |
Show(class_r0, number_r0, name_r0, math_r0, class_r1, number_r1, name_r1, cast_i32(math_r1));

試験的なモジュールのサポート

遊び半分で、本ライブラリ(Parser部分を除く)をモジュールとして使用できるようにした。ただし、ADAPTをCMakeでインストールする際に-ENABLE_MODULE=ONとオプションを与える必要がある。さらに現時点でビルド可能なのはClang>=20とGCC>=15のみで、MSVCでは現状ビルド不能である。"sorry: not yet implemented"て……。

find_package(OpenADAPT REQUIRED)
target_link_libraries(myproj PRIVATE OpenADAPT::Module)
//このあたりのヘッダは事前にincludeする必要がある。
#include <format>
#include <iostream>
#include <vector>
#include <string>
#include <cassert>
#include <cmath>
#include <complex>
#include <ranges>
#include <random>
#include <filesystem>
#include <thread>
//モジュールからはマクロをimportできないので、独立にincludeする。
#include <OpenADAPT/Macros.h>
import adapt;

ものは試しと色々頑張って使えるようにしてみたものの、私にはほぼ何の恩恵もなかったので今後サポートし続けるかは不明である。

与太話

文字列からラムダ関数を作る機能って誰が使うの、と甚だ疑問に思うような更新内容だが、私が使うのである。ADAPT v1以前にはそもそもこちらの文字列式から生成するラムダ関数機能しかなかった。というのもADAPTは本来、パラメータファイルなどから文字列情報として計算式を受け取ることを念頭に開発されていたのだ。これはADAPTが昔私が開発していたいくつかの研究用GUIアプリケーションのベースライブラリとなっており、JSONないしYAMLファイルで挙動を記述する必要があったので、そのような設計にならざるを得なかったためである。
OpenADAPTの開発に当たっては、それらの研究用アプリケーションは概ね安定動作し喫緊の開発課題でなくなっていたことで、一旦文字列からのラムダ関数機能は捨て去ることにして、パーサーを端折った実装にした。データ分析ライブラリとしてはこちらのほうが使い勝手が良かったのはまあ、うん。
ただ、諸般の事情でそろそろ上記研究用アプリケーションをアップデートしたいと思い始め、そのためにはOpenADAPTにも文字列式パーサーを実装しなくてはならず、二年越しに重い腰を上げたのである。

この研究用アプリケーションのうち一つは3Dデータビューアだ。主として我々が使用する検出器の情報を3Dで可視化するために作成したGUIアプリケーションであるが、一応どのようなデータでも扱える汎用設計になっており、その肝となっていたのがこの文字列式パーサーだった。これにより、生データをどのような三次元情報に変換するかをパラメータとして記述させていたのだ。
何もかもが思惑通りに進んだ場合、いずれこのツールもオープンソースで公開するかもしれない。それこそ誰が使うの、という話にはなってしまうけれども。

ちなみに今回、パーサーのコードの大半はGitHub CopilotのAgentモードを使用して生成させた。私もAIによるコーディングは色々と試してきたのだが今の今まで上手く行った試しがなく、単純な反復コードを予測させる以外に活用することはほとんどなかった。しかしAgentモードでは自身でテストプログラムを作成、ビルド、実行、エラー修正を反復してくれるようになり大幅に精度が向上したので、これなら実用的だと思い取り入れてみることにした。
尤も、ある程度コードが複雑化すると精度は頭打ちになった。こちらが作れといった機能を作らず保留したり、一度修正させた箇所をわざわざ元の愚劣なコードに戻したり、ライブラリの構造を理解しきれず意味不明な呼び出しを試みたり、過去の記憶をすっかり忘却して同じミスを何度も何度も繰り返したり、速度以外は人間の足元にも及ばない状態ではあった。よって、精度が頭打ちになったあたりでAIに修正させるのを止め、この時点での出力を叩き台として私が大幅な修正を施した。

現時点ではそれほど大規模でないコードの叩き台を作らせるくらいの役割が精一杯であろう。一方で自分では思いつかなかった効果的な実装を提案してくれる場合もあるなど、時間短縮以上のメリットも感じられた。凄まじく仕事が早く思考がフラットである代わりに理解力と記憶力が極めて悪い、アンバランスな部下ができたと思えば、使いようによっては大幅な作業効率化が果たせそうではあった。

……Gitの履歴やGitHubのContributorsにCopilotが載ってしまうのは、ちょっと気持ち悪いなぁと思わなくもないけれども、現状解決方法がなさそうだったので諦めることにした。まあ実際に貢献してくれているのだから、人ではないという理由で排除するのはよくないかもしれない。いや別にAIに人格を認めたりする気はないのだが。


  1. キーワード__declspec(dllimport)を有効化するためのマクロ。
  2. 尤もcount_if関数などで事足りているので、私自身活用したことは多くない。
  3. 厳密に言えば、連結していないDTree等とは振る舞いが一致しない状況が出てくるので、実装をためらっている。まあ現時点ですでに.atなどで異なる振る舞いをしているので、今更かもしれないが……。

[C++][CMake]モジュールを使用するCMakeプロジェクトの作成。

前回、ヘッダオンリーライブラリをC++20のモジュールとして使用可能にする記事を書いた。謎にXのポストが伸びてしまったあたり、みんなモジュールについて「興味はなくもないけど使ってはいない」んだなぁとよく分かった。実際私もその一人だったのだから何も言えない。

さて、C++でのビルド環境といえば不幸にもCMakeがデファクトスタンダードとなってしまっているので、あの奇々怪々なコマンドに適応しなければモジュールを使えるようになったとは言えないだろう。2025年にもなってこの書き方の解説さえ満足に存在しない状況であったため、こちらも簡単にまとめておこうと思う。特にinstallfind_packageの周りがモジュールではどうなっているのか、そのあたりを掘り下げてみる。

なお、CMakeは3.28でモジュールに本格対応したのだが、残念ながらここで解説する使い方には3.29以上が必要だと思われる。今回紹介する方法は、私の手元の環境だと、CMake 3.28ではエラーが出て失敗し、CMake 3.31では成功した。ネット上で調べた限りCMake 3.29でエラーが解消されたという報告があったので、少なくともそれ以上を勧めておく。
(本当はこの記事は前回の記事と統合して上げるつもりだったのだが、このCMakeのバグに引っかかり原因を理解できず、やむなくCMake部分だけ後回しにしたものだったりする。)

1. 自身のCMakeプロジェクトにモジュールのソースコードが存在する場合

もしモジュールのソースコードが全てプロジェクト内に存在するのなら、次のように書けば良い。今回、mymodule.ixxをモジュールのソースコードとして使用し、これをMyAppにリンクする形で使用している。 target_sources(... FILE_SET modules TYPE CXX_MODULES ...)が肝で、他は通常のライブラリを使用する場合と大差ない。

add_library(MyModule)

target_compile_features(MyModule PRIVATE cxx_std_20)#PUBLICでもいいだろうとは思うが。

target_sources(
    MyModule PUBLIC
    FILE_SET modules
    TYPE CXX_MODULES
    FILES "mymodule.ixx")

add_executable(MyApp "myapp.cpp")

target_compile_features(MyApp PRIVATE cxx_std_20)

target_link_libraries(MyApp PRIVATE MyModule)

2. モジュールを配布する/配布されているモジュールを使用する場合

もちろん、1.のような状況ばかりではないだろう。モジュールを配布したり、配布されているモジュールを導入したい場合だってあるはずだ。例えば私は普段、自作ライブラリをinstallコマンドで特定の場所に配置させるようにしているし、find_packageコマンドで別のプロジェクトから使用できるようにもしている。折角なのでモジュールもこれに対応させたい。さらに、本記事は前回に引き続きヘッダオンリーライブラリをモジュール化するという趣旨で書いている。よって、ここでもやはり#includeimportのどちらからでも使用できるようにする。

まず、次のようなディレクトリ構造を想定しよう。MyLib/MyLibはライブラリのヘッダファイルmylib.hがあり、MyLib/MyModuleにはそのヘッダをモジュール化したmymodule.ixxが入っている。

MyLib/
    MyLib/
        CMakeLists.txt
        mylib.h
    MyModule/
        CMakeLists.txt
        mymodule.ixx
    CMakeLists.txt

最終目標は、find_package(MyLib REQUIRED)としてこれらのライブラリを発見し、かつtarget_link_libraries(... MyLib::MyLib)とすればヘッダオンリーライブラリとして、target_link_libraries(... MyLib::MyModule)とすればモジュールとして使用できることだ。このようにすれば、単一のパッケージで#include版とimport版を同時に配布することができる。mylib.hmymodule.ixxの中身は前回の記事を参照してほしい。

project(MyLibProject)

#MyModuleはデフォルトでは無効。
#USE_MODULE=ONというオプションを与えた場合のみ、MyModuleを有効にする。
option(USE_MODULE "use MyLib module" OFF)

add_subdirectory ("MyLib")
if(USE_MODULE)
    add_subdirectory ("MyModule")
endif()

install(
    EXPORT MyLibConfig
    DESTINATION cmake
    NAMESPACE MyLib::)
#MyLib/CMakeLists.txt
project(MyLib)

add_library(MyLib INTERFACE)
add_library(MyLib::MyLib ALIAS MyLib)

target_include_directories(MyLib
    INTERFACE
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/..>
    $<INSTALL_INTERFACE:include/MyLib>)
set_target_properties(
    MyLib
    PROPERTIES
        PUBLIC_HEADER "mylib.h")
install(
    TARGETS MyLib
    EXPORT MyLibConfig
    INCLUDES DESTINATION include/MyLib
    PUBLIC_HEADER DESTINATION include/MyLib/MyLib)
#MyModule/CMakeLists.txt
project(MyModule)

add_library(MyModule)

target_compile_features(MyModule PRIVATE cxx_std_20)

target_link_libraries(MyModule PUBLIC MyLib)

target_sources(
    MyModule PUBLIC
    FILE_SET modules
    TYPE CXX_MODULES
    FILES "mymodule.ixx")

add_library(MyLib::MyModule ALIAS MyModule)

install(
    TARGETS MyModule
    EXPORT MyLibConfig
    RUNTIME DESTINATION bin/${CMAKE_BUILD_TYPE}
    LIBRARY DESTINATION lib/${CMAKE_BUILD_TYPE}
    ARCHIVE DESTINATION lib/${CMAKE_BUILD_TYPE}
    CXX_MODULES_BMI DESTINATION module/${CMAKE_BUILD_TYPE}
    FILE_SET modules DESTINATION include/MyLib/MyModule)

C++17以下であったりコンパイラが対応できていなかったりなどの理由でモジュールをビルドできない環境にも対応するため、-USE_MODULE=ONというオプションを与えた場合のみMyModuleがビルド、インストールされるようにしている。特に重要なのはinstall(... FILE_SET modules DESTINATION include/MyLib/MyModule)の部分である。現状、外部からモジュールを使用するためには.ixxなどのモジュールのソースコードが不可欠で、これをインストールして他のプロジェクトから参照可能にしておく必要がある。

このパッケージをインストールした場合、例えばGCCでDebugビルドした場合なら、次のようにファイルが配置される。

install_dst/
    cmake/
        MyLibConfig.cmake
        MyLibConfig-debug.cmake
    include/
        MyLib/
            MyLib/
                mylib.h
            MyModule/
                mymodule.ixx
    lib/
        Debug/
            libMyModule.a
    module/
        Debug/
            mymodule.gcm

モジュールのソースコード.ixxBMIファイル.gcmの配置については鉄板と言える場所を知らないので適当に設定した。また現行のビルドシステム的には、.gcmlibMyModule.aは使用されているのか謎である。特にBMIに関しては、コンパイラのバージョンなどで完全に統一的なファイルを出力することが困難であるため、実効的には用いられておらず、ユーザーが都度.ixxから生成する必要がある、とのことである1。モジュールの魅力が半減してしまう気がするが……。

こうしてインストールしたライブラリを別のCMakeプロジェクトから使用する場合は以下のように書く。

find_package(MyLib REQUIRED)

add_executable(MyApp "myapp.cpp")

target_compile_features(MyApp PRIVATE cxx_std_20)

#ここでMyLib::MyModuleを指定すれば`import`で、
#MyLib::MyLibを指定すれば`#include`で使用可能になる。
target_link_libraries(MyApp PRIVATE MyLib::MyModule)

特にモジュールであることを意識する必要はない。なおCMake 3.28を使っている場合、MyAppからmymodule.ixxコンパイルする最中にコンパイルエラーとなった。

余談

そろそろCMakeより圧倒的に使いやすいビルドシステム、パッケージマネージャに出てきてほしい。というより、それが実現しなければRustに立場を奪われてC++が滅び去るだけだと思う。そう思わずにはいられないくらい、CMakeのコマンドは理解しがたい。
本記事の内容が5年後には意味を成さなくなっていることを願いつつ、あるいは憂いつつ、せめて今この瞬間に必要な人の助けとなるよう。

[C++]ヘッダオンリーライブラリをモジュール化する方法の備忘録。

本記事には著者の偏った知識、不十分な調査、AIのハルシネーション1などによる誤った記述が含まれる可能性があります。参考にするとしても決して鵜呑みにしないでください。また誤りや非効率を発見した場合は指摘してもらえると大変助かります。

こんな言い訳ばかり書き並べて責任から逃げ続けているからQiitaとかZennとかに移住できないんだろって?AdC参加しないチキン野郎?はい。

方法

前提として、ある一つのヘッダオンリーライブラリを#includeimportのどちらでも使用可能にする、という方針を挙げておく。C++20以上を要求するライブラリはまだそれほど主流ではないだろうし、ビルド環境の都合でC++20を使用しているがモジュールを使用できない場合などもあるかもしれない(私の環境がまさにこれ)。

なお今回はあくまでC++コードの記述についてのみ扱う。次回の記事ではCMakeの記述方法やビルド、インストール、find_packageの使い方などを解説する。

次のヘッダーファイルをモジュール化することを考えてみる。 特に決まり切った簡単な方法があるわけではないようで、ライブラリの設計や規模に合わせて何通りかの方法で対応していくしかなさそうである。

//mylib.h
#ifndef MYLIB_H
#define MYLIB_H

#include <string>

namespace mylib
{

int my_func(int i) { return i * 2; }

template <class T>
T my_func_template(T a, T b) { return a + b; }

class MyClass
{
public:
    inline MyClass(const std::string& v) : value(v) {}
    inline const std::string& GetValue() const { return value; }
private:
    std::string value;
};

template <class T>
class MyClassTemplate
{
public:
    MyClassTemplate(T v) : value(v) {}
    T GetValue() const { return value; }
private:
    T value;
};

}

#endif

方法1. プライマリモジュールインターフェイスで必要なものを個別にエクスポートする

インターフェイス部分でのクラスや関数の宣言にexportを付与しておくと、その実装も暗黙的にエクスポートされるという仕組みがある。したがって、各ヘッダファイル内の関数やクラスにいちいちexportを付けずとも、必要なものだけをインターフェイスに書き下しておくという方法は使える。その後、ヘッダの方は特に改変などはせず末尾で#includeするだけでよい。

//mymodule.ixx
module;

#include <string>

export module mymodule;

//#define MYLIB_EXPORT export

namespace mylib
{
export int func(int i);
export template <class T> T func_template(T a, T b);
export class MyClass;
export template <class T> class MyClassTemplate;
}

#include <MyLib/mylib.h>

export対象が少ない場合はこれで十分だろうと思う。宣言一つ一つにexportを付与するのが大変なら、上記の名前空間mylibを丸ごとエクスポートするという手もある。私がしばしば使うankerl::unordered_denseではこの方法が採用されていた。

方法2. クラスや関数ごとにexportキーワードを付与する

ライブラリが巨大化してくると、公開したい関数やクラスが膨大になってきて、モジュールインターフェイスの方でいちいち書き並べるのが大変になる。このような場合はいっそ、素直に個々の関数やクラスにexportキーワードを与えていく方が管理しやすいだろう。

もちろん、モジュールとして使わない(#includeして使う)場合にはexportキーワードは邪魔なので、マクロで切り替える仕様にするのが望ましい。この場合、モジュールインターフェイス#define MYLIB_EXPORT exportのように定義しておき、モジュールとして使用されいない場合は#define MYLIB_EXPORTと空になるよう調整する必要がある。

//mylib.h
#ifndef MYLIB_H
#define MYLIB_H

#include <string>

#ifndef MYLIB_EXPORT
#define MYLIB_EXPORT
#endif

namespace mylib
{

MYLIB_EXPORT
int my_func(int i) { return i * 2; }

MYLIB_EXPORT
template <class T>
T my_func_template(T a, T b) { return a + b; }

MYLIB_EXPORT
class MyClass
{
public:
    inline MyClass(const std::string& v) : value(v) {}
    inline const std::string& GetValue() const { return value; }
private:
    std::string value;
};

MYLIB_EXPORT
template <class T>
class MyClassTemplate
{
public:
    MyClassTemplate(T v) : value(v) {}
    T GetValue() const { return value; }
private:
    T value;
};

}

#endif
//mymodule.ixx
module;

#include <string>

export module mymodule;

#define MYLIB_EXPORT export

#include <MyLib/mylib.h>

今回、私のライブラリは方法1.を使用するには巨大すぎたので、こちらを採用した。

なおmylib.hの方の名前空間スコープを全てexportするという方法は、可能だが、推奨されないとするやり取りがあった2
重大な問題として考えられるのは、名前空間に含まれるものすべてがexportされる点は挙げられる。例えば本来モジュールにおいては非公開とすることが推奨されるdetail名前空間などに格納されているものまですべてAPIとして公開されてしまう。せっかく実装部分を隠蔽する手段を手に入れたというのに自ら晒してしまうとは何事だ、という意見は分かる。
とはいえ、問題が本当にそれだけなら#includeする場合と同等になるだけなので、実効的には困らないのではとも思ってしまうが。

その他注意点

ライブラリ内で#includeしているものは全てグローバルモジュールフラグメントにも書き下す

モジュール全般に言えることのようだが、#includeしたものはソースコード内に愚直に展開されてしまうため、ライブラリのコード内で#includeしたものはそのままだとモジュール内に含まれてしまう。これは単純にモジュールの肥大化を招くだけでなく、ODR違反を引き起こす可能性もある3

そのため、大変面倒ではあるが、これらはグローバルモジュールフラグメントに全て書き下しておく必要がある。こうすると、ライブラリのコードの方ではインクルードガードで展開が抑制されるので、上記の問題を抑制できる。またその他のコード中の#includeは消さなくても良いので、ヘッダオンリーライブラリとして使いたい場合も両立可能である。

//mymodule.ixx
module;

#include <string>// <-mylib.hでincludeしているヘッダを、全てここでもincludeしておく。

export module mymodule;

#define MYLIB_EXPORT export

#include <MyLib/mylib.h>

マクロはexportできないため、別途#include可能に

総じて見れば朗報ではあるのだが、マクロはモジュールの外には公開されない。今までマクロを定義するたびに名前衝突の恐怖に怯えていたあの瞬間はモジュールにおいてはもう訪れない。……そして、マクロをエクスポートする術が提供されていない以上、マクロを公開したい場合は独立したヘッダに書き並べておき、ユーザーに別途#includeさせる必要がある。

非テンプレートのメンバ関数にはinlineを

一般に非テンプレートのクラス定義スコープ内に入れ込まれた非テンプレートメンバ関数定義は暗黙的にinlineが与えられたものとみなされていたが、モジュールではそうならないらしい。そのため、もしインライン展開されることを望むなら明示的にinlineを与えておく必要がある。裏取りが十分でないので誰か情報源求む。onihusube9様より情報をご提供いただきました4

class A
{
    //#includeする場合は暗黙的にinlineと見做されていたが、モジュールとして使う場合はinlineと扱われない。
    //必要なら、明示的にinlineを与えておくこと。
    void func() {}
};

モチベーション

コンパイルが重い!
とかくコンパイルが重いのである。私が作成しているADAPTというデータ分析・可視化のためのライブラリの話だ。
本ライブラリはテンプレートを多用するヘッダオンリーライブラリであるが、その複雑な設計も相まってビルド時間がどんどん伸びてしまっている。テストプログラムのビルドに数分かかるようになってきた。もっと問題なのはGCCのエラーだ。本ライブラリは一応クロスプラットフォーム開発を行うつもりで、WSL上でGCCとClangによる動作確認も行っているのだが、最近はGCCだと重すぎてビルドが通らなくなってきた。コードそのものは問題ないはずで、MSVCとClangでは普通にテストも通るのだけれども、GCCでのビルドだけは私のPCのメモリ32GBをすべて食いつぶし、数十分も処理を続けて、その挙げ句にエラーを吐いて失敗するのである。これまでは何度もビルドし直しているとそのうち成功したのだが、先日は何度繰り返してもエラー、エラー、エラー、ついぞ一度も成功しなかった。なんてコンパイラだ、と言いたいところだが私のコードが雑すぎるのが原因なので何も言えない。

そんなわけで自作ライブラリの重さをちょっとでも改善できないかとモジュール化を試していたわけである。
ただ困ったことに、多数の問題が見えてきた。まず私のライブラリはMSVCとGCCではモジュール化出来なかった。Clangではテストプログラムまで通ったものの、MSVCには"sorry: not yet impletmented."と言われ、GCCではよく分からないエラーが生じ、どちらもコンパイルが通らなかった。今回はGCC 14を使用したが、15で大幅なモジュールサポートの改善があったらしいので、時間を見つけてそちらを試してみたい。
またCMakeを使ってライブラリをモジュールとしてコンパイルし、それを別のCMakeプロジェクトからインポートすることも試したが、こちらは逆にMSVCでのみ成功し、ClangとGCCでは失敗した。……ただ、MSVC、Clang、GCCいずれも折角生成されたBMIを無視して.ixxを直接読み込んでいるようで、なかなか謎めいた挙動をしており、現状、いずれのコンパイラもCMakeもモジュールを満足に扱える状態でないように思えている。
折角身につけた知識を無駄にするのも腹立たしいのでもう少し悪あがきしてみようと思うが、恐らく私は当面自作ライブラリのモジュール化を断念せざるを得ないだろう。

結局、Clang18 & 20以上とGCC15でのみ何とかモジュール化できたが、MSVCではできなかった。MSVCは"sorry: not yet impletmented."という謎のエラーを吐き出し、私のコーディングで回避可能なのかさえ分からなかったので、断念する他なかった。 さらに、結論として、GCCのビルドが軽くなったりなどは一切しなかった。いや私が体感できないレベルで改善していたのかもしれないが、莫大なメモリを食いつぶし異常終了することに変わりはなかった。ChatGPTが言うにはモジュール化はClangやMSVCでは有効ながらGCCではむしろ悪化する可能性があるという話だったが、これについては信頼できるのかどうか不明である。
私がメインで使用している環境はMSVCで恩恵がなく、改善を狙ったGCCでは効果がなかった。骨折り損の草臥儲。私は何週間も一体何をしていたのか。……まあ折角なので、件のライブラリは「試験的にモジュールに対応させた」という体でモジュールとして使用できるようにしておいた。

もう2026年になるというのに、C++20の機能を未だ満足に使えないとは。我々は一体いつまで待てばいいのだろう。特にC++26には是非使いたい機能が目白押しなのだが、実際に導入できるのは2030年とかになったりするのだろうか。その頃、私はこのライブラリのメンテナンスを継続しているのだろうか。