🍛

C++ のモジュールインターフェースパーティションの使いどころ(2)

2022/05/29に公開

C++ のモジュールインターフェースパーティションの使いどころ

前回の記事で、基本的にはインターフェースファイルごとにモジュールを宣言するほうが良いということを書きました。

パーティションを使用するとモジュールインターフェースを複数のファイルに分割できますが、それぞれが別々のモジュールであってもひとつのモジュールでexport import によりまとめてしまえば同じように扱えるので、個別のインポートもできるようにモジュールは分けておいたほうが良さそうである、ということでした。

それではモジュールインターフェースパーティションを使うような場面はあるのか、ということを調べました。

複数のファイルで構成されたリンクリストのプログラム

以下のような独自のリンクリストを作成します。環境はVisual Studio 2022 です。

Node.ixx
export module Node;
/// リンクリストの要素を持つノード
export struct Node {
    void * ptr;     ///< 保持する要素のアドレス
    void * next;    ///< 次のノード
    void * prev;    ///< 前のノード
};
LinkList.ixx
export module LinkList;
export import Node;
/// リンクリスト
class LinkList {
public:
    /// 要素を追加
    void Add(void * ptr);
    /// 要素を削除
    void Remove(void * ptr);
    /// 要素数を取得
    int Count() const;
    /// 要素を取得
    void * Get(int index) const;
private:
    Node * node_{ nullptr };
};
LinkList.cpp
module LinkList;
// 要素を追加
void LinkList::Add(void * ptr)
{
    /* Node をnew してnode_ に接続する処理 */
}
// 要素を削除
void LinkList::Remove(void * ptr)
{
    /* node_ からptr を保持するノードを探してdelete */
}
// 要素数を取得
int LinkList::Count() const
{
    /* 要素数をカウントして返す */
}
// 要素を取得
void * LinkList::Get(int index) const
{
    /* 先頭からindex 番目の要素を返す */
}

LinkList クラスは内部でNode 構造体のリンクリストを管理しています。LinkList クラスの各メンバ関数の実装は今回の話に重要でないので省略しています。また、あまり実用的ではありませんが便宜上保持する要素は生のvoid ポインタとしています。

このLinkList は問題なくビルドして実行することができます。しかしNode 構造体はLinkList 内部でしか使用しないのでLinkList モジュール以外からアクセスできないよう変更することを考えます。

まず変更前のLinkList、Node は以下のように使用できます。

main.cpp
import LinkList;

int main()
{
    LinkList linkList;
    char c[] { "char" };
    linkList.Add(c);
    linkList.Remove(c);

    Node node; // 特に意味はないがエラーではない
}

LinkList モジュールをインポートしていると、Node 構造体にもアクセス可能でインスタンスが作成できます。main 関数からNode にアクセスできないように変更します。

まずmain.cpp ではLinkList のみインポートしています。LinkList ではNode をexport import としていますのでexport を削除してみます。

LinkList.ixx
export module LinkList;
import Node;

確認したMSVC 環境だとmain.cpp で引き続きNode にアクセスは可能でした。これはエクスポートされているLinkList クラスにNode のポインタが含まれていることが原因のようです。

また直接Node モジュールをインポートすればmain.cpp からNode 構造体にはアクセスできます。かといってNode 構造体をエクスポートしなければLinkList モジュールからも当然アクセスはできなくなりますからNode モジュールでNode 構造体をエクスポートしないという方法は選択できません。

モジュールパーティションを使用する

ここで、Node モジュールをLinkList モジュールのパーティションに変更してみます。また、Node 構造体はエクスポートしないよう変更します。

Node.ixx
export module LinkList:Node;
struct Node {
    // メンバ変数は同じ
};
LinkList.ixx
export module LinkList;
export import :Node;
// LinkList クラスは変更なし

Node 構造体はエクスポートしていませんが、LinkList モジュール内のLinkList クラスからはアクセス可能です[1]。こうすればNode 構造体にアクセスできる範囲をLinkList モジュール内に限定できます。

結論

このようにモジュール内からのみアクセスしたいクラス、構造体、その他を、別のファイルに分けておきたい場合というのがモジュールインターフェースパーティションの使いどころといえそうです。

補足

ただしNode 構造体にモジュール外からアクセスできなくするだけであれば、この程度の構造体であればLinkList クラス内でprivate アクセスで定義したり、

LinkList.ixx
export module LinkList;

export class LinkList {
public:
    // 関数宣言
private:
    struct Node {
        void * ptr;
        Node * next;
        Node * prev;
    };
    Node * node_{ nullptr };
};

LinkList.ixx で前方宣言し、LinkList.cpp で定義するなどの実装方法も考えられます。

LinkList.ixx
export module LinkList;

struct Node; // 前方宣言、エクスポートはしない

// LinkList クラスは変更なし
LinkList.cpp
module LinkList;

struct Node {
    void * ptr;     ///< 保持する要素のアドレス
    void * next;    ///< 次のノード
    void * prev;    ///< 前のノード
};

// LinkList の実装は変更なし

また、Visual Studio 2022 に含まれていたclang++ (バージョン13.0.0) ではモジュールパーティションはサポートされていませんでした。

今のところ調べた範囲ではモジュールインターフェースパーティションの活躍する場面というのは限られているようです。

脚注
  1. https://cpprefjp.github.io/lang/cpp20/modules.html によると「パーティション内の宣言はエクスポートしていなくても見える」ということです。 ↩︎

Discussion