以下の内容はhttps://kenkyu-note.hatenablog.com/entry/2025/12/22/214503より取得しました。


[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年とかになったりするのだろうか。その頃、私はこのライブラリのメンテナンスを継続しているのだろうか。




以上の内容はhttps://kenkyu-note.hatenablog.com/entry/2025/12/22/214503より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14