[C++] モジュールへの移行を考える 4 - 単一ヘッダファイル+複数ソースファイル


目次(予定)

  1. 単一ヘッダファイル+単一ソースファイル
  2. 実装の隠蔽について
  3. 複数ヘッダファイル+単一ソースファイル
  4. 単一ヘッダファイル+複数ソースファイル(この記事)
  5. 複数ヘッダファイル+複数ソースファイル
  6. ヘッダオンリー
  7. モジュールとヘッダファイルの同時提供

単一ヘッダファイル+複数ソースファイル

/// mylib.h

namespace mylib {

  class S {
    int m_num = 0;
  public:

    S();
    S(int n);

    int get() const;
  };

  void print_S(const S& s);
}
/// mylib1.cpp

#include "mylib.h"

namespace mylib {

  S::S() = default;
  S::S(int n) : m_num{n} {}

  inline int S::get() const {
    return this->m_num;
  }
}
/// mylib2.cpp

#include <iostream>
#include "mylib.h"

namespace mylib {

  void print_S(const S& s) {
    std::cout << s.get() << std::endl;
  }
}

例えばこんな風に、1つのヘッダに対して複数の実装ファイルが対応している場合の事です。

前回と同様に、実装単位もまたパーティションによって分割することができます。実装単位の場合は実装パーティションが対応します。

/// mylib.ixx

// mulibモジュールのインターフェース単位
export module mylib;

namespace mylib {

  // クラスのエクスポート、暗黙に全メンバがエクスポートされる
  export class S {
    int m_num = 0;
  public:

    S();
    S(int n);

    int get() const;
  };

  // フリー関数のエクスポート
  export void print_S(const S& s);
}
/// mylib1.cpp

// mulibモジュールの実装パーティション1
module mylib:part1;

// インターフェースのインポート
import mylib;

namespace mylib {

  S::S() = default;
  S::S(int n) : m_num{n} {}

  inline int S::get() const {
    return this->m_num;
  }
}
/// mylib2.cpp

module; // グローバルモジュールフラグメント

// #includeはグローバルモジュールフラグメント内で行う
#include <iostream>

// mulibモジュールの実装パーティション2
module mylib:part2;

// インターフェースのインポート
import mylib;

namespace mylib {

  void print_S(const S& s) {
    std::cout << s.get() << std::endl;
  }
}

モジュール実装パーティションであることは、exportのないモジュール宣言においてモジュール名の後に:パーティション名を続けることで行います。

// モジュール実装パーティションの宣言
module mylib:part1;
module mylib:part2;

// モジュール実装単位の宣言
module mylib;

なお、インターフェスパーティションも含めて同じモジュール内でパーティション名は被らないようにしましょう(ここではpart1とかいう名前にしましたけど、もっとちゃんとした名前つけましょう)。

非パーティションの実装単位と区別するために、パーティションによる実装単位の事をモジュール実装パーティションと呼び、そうでない実装単位の事を単にモジュール実装単位と呼びます。

通常の実装単位とは異なり、実装パーティションはそのモジュールのプライマリインターフェースを暗黙にインポートしていないので、明示的にインポートする必要があります。

これらの事以外は通常の実装単位、あるいは普通のソースファイルと同様に書くことができます。

別の書き方

2つのソースファイルを2つの実装パーティションに対応させましたが、実は実装単位は複数存在できます。両方を実装単位にしても構いません。

/// mylib1.cpp

// mulibモジュールの実装単位
module mylib;

namespace mylib {

  S::S() = default;
  S::S(int n) : m_num{n} {}

  inline int S::get() const {
    return this->m_num;
  }
}
/// mylib2.cpp

module; // グローバルモジュールフラグメント

// #includeはグローバルモジュールフラグメント内で行う
#include <iostream>

// mulibモジュールの実装単位
module mylib;

namespace mylib {

  void print_S(const S& s) {
    std::cout << s.get() << std::endl;
  }
}

(インターフェース単位を省略しています)

この様に書いたとしても、先程と同様のモジュールを構成することができます。実装単位と実装パーティションの使いどころの差については次回ご説明します。

利用側

インターフェスパーティションへの分割の時と同様に利用側は全く変わりません。

import mylib; // mylibモジュールのインポート宣言

int main() {
  mylib::S s1{};
  mylib::S s2{20};
  
  mylib::print_S(s1); // 0
  mylib::print_S(s2); // 20
}

インターフェスパーティションも実装パーティションも実装単位も、モジュールの中でどのように分割しようともモジュール外部からは分かりません。モジュールの構成は好きなようにすることができます。

ここまで見てみると、モジュール外部に公開されるのはそのインターフェースだけである事が分かります。特に、モジュールのプライマリインターフェース単位はモジュール外部に公開される唯一のものです。そして、その公開も#includeのような直接的なものではなく、importを介したごく間接的な形で行われます。

パーティション、まとめ

ここまでで4+1種類のモジュールの形式が登場しました。ユーザーが書く部分のモジュールとしてはこれで全てです。しかし、似たような言葉が飛び交ってややこしく理解しづらいと思うので簡単にまとめておきます。

種別 宣言例 export宣言
プライマリインターフェース単位 export module M; 1(必須) 書ける
実装単位 module M; 0 <= 書けない
インターフェスパーティション export module M:interface; 0 <= 書ける
実装パーティション module M:impl; 0 <= 書けない
プライベートモジュールフラグメント module : private; 1(存在する場合) 書けない