- cpp20[meta cpp]
C++20では、ヘッダーファイル・ソースファイルに代わる新たなファイル分割の仕組みとしてモジュールが導入された。
モジュールは翻訳単位の先頭でモジュール宣言を行うことにより宣言する。
export module mylib;
モジュールの中では、関数や型などの宣言に export
キーワードを付けて宣言をエクスポートできる。
export module mylib;
namespace mylib {
export struct myfunc_result_t {
int x, y;
};
export myfunc_result_t myfunc() { /*...*/ };
};
モジュールを利用するには、インポート宣言を行う。これにより、インポートしたモジュールがエクスポートしている宣言が見えるようになる。
import mylib;
int main() {
// これらの型や関数の宣言はこの翻訳単位には無いが、
// mylibでエクスポートしているので、使用することができる。
mylib::myfunc_result_t ret = mylib::myfunc();
}
モジュールは単一の翻訳単位で構成することも、複数の翻訳単位で構成することもできる。
C++20では標準ライブラリはモジュール化されないが、その中でC++ライブラリはヘッダーユニットとしてインポートできる。 標準ライブラリのモジュール化はC++23以降に予定されている。
モジュール宣言の構文は以下のようになる:
export(opt) module モジュール名 属性(opt);
- モジュール宣言は翻訳単位あたり1回だけ、原則として翻訳単位の先頭に記述する。
- モジュール宣言を含む翻訳単位をモジュールユニットという。
export
がある場合をモジュールインターフェースユニット、ない場合をモジュール実装ユニットと呼ぶ。- あるモジュールについて、モジュールインターフェースユニットがただ1つ存在しなければならない。モジュールの実装は好きなだけ存在できる。
- モジュール実装ユニットはモジュールインターフェースユニットを暗黙的にインポートする。
export module foo; // fooのモジュールインターフェースユニット
module foo; // fooのモジュール実装ユニット
module foo.bar; // foo.barのモジュール実装ユニット
export module bar [[deprecated]]; // 属性
モジュール名は、識別子または識別子をドットで繋いだもの(例えば、foo
やstd.core
)である。
std
とstd
から始まるあらゆるモジュール名及び予約語を含むモジュール名は、今後の規格や処理系のために予約されているので、ユーザー定義のモジュール名として使うことはできない。- モジュールの名前は、モジュールに属する型、関数などの名前とは無関係である。
- 処理系の中には、モジュールユニットのファイル名とモジュール名が揃っていることを期待するものがある(そうでない場合は追加のコマンドラインオプションが必要になる)。
モジュール名にはドットを使いたいが、一方で識別子にはドットが使えない。このような矛盾があるため、モジュール名にドットを使うとトークンが分割される。 そのため、識別子の規則は分割されたそれぞれのトークンに適用される。
module foo . bar; // OK. 'foo.bar'と等しい
module foo . /*comment*/ bar; // OK. 'foo.bar'と等しい
module foo . . bar; // NG. ドットの間に識別子がない
module _Foo.bar // NG. 識別子'_Foo'は予約されている
module foo__bar // NG. 識別子'foo__bar'は予約されている
module foo.__bar.baz // NG. 識別子'__bar'は予約されている
module foo.version.0 // NG. 識別子の1文字目は数字にできない
プライベートモジュールフラグメントは、1ファイルでモジュールを定義しつつインターフェースと実装を分離するための機能である。
export module foo;
// モジュールのインターフェース
module :private;
// プライベートモジュールフラグメント
プライベートモジュールフラグメントを記述する場合、そのモジュールは翻訳単位(必然的にモジュールインターフェースユニット)を1つしか持つことができない。
C++20では、名前のあるモジュールに属していない宣言はグローバルモジュールに属している。
グローバルモジュールの性質は以下の通り。
- 名前を持たず、インポートすることはできない。
- 宣言をエクスポートすることはできない。
- モジュールインターフェースユニットを持つことはできない。
グローバルモジュールとは、要するに従来通りの、モジュールではないコードのことである。C++20では、main
関数はグローバルモジュールに属していなければならない。
宣言の前にexport
キーワードを付加することでその宣言をエクスポートできる。
エクスポート宣言はモジュールインターフェースユニット内の名前空間スコープで行える。ただし、以下の場所では不可。
- グローバルモジュールフラグメントの中
- プライベートモジュールフラグメントの中
- 無名名前空間の中
export int x; // 変数のエクスポート
export class x{ /*...*/ }; // クラスのエクスポート
export namespace x { /*...*/ } // 名前空間のエクスポート
export template<class T> foobar(); // 関数テンプレートのエクスポート
エクスポートできるのは新たな名前を導入する宣言のみである。ただし、その名前は内部リンケージを持ってはいけない。
// P1103R3より引用
export module M;
export namespace {} // エラー: 新たな名前を宣言していない
export namespace {
int a1; // エラー: 内部リンケージを持つ名前はエクスポートできない
}
namespace {
export int a2; // エラー: 内部リンケージを持つ名前はエクスポートできない
}
export namespace N { // OK
int x; // OK: エクスポートされる
static_assert(1 == 1); // エラー: 新たな名前を宣言していない
}
export static int b; // エラー: 明示的にstaticで宣言されている名前はエクスポートできない
export int f(); // OK
export namespace N { } // OK
export using namespace N; // エラー: 新たな名前を宣言していない
また、波カッコにexport
をつけることで、その中の宣言をまとめてエクスポートできる。
- この波カッコはスコープを作らない
export {
struct Foo { /*...*/ }; // エクスポートされる
static_assert(std::is_trivially_copyable_v<Foo>); // エラー: 新たな名前を宣言していない
using namespase std; // エラー: 新たな名前を宣言していない
}
まとめると、次のような宣言はエクスポートされる。
- 明示的にexport宣言されている宣言
- 明示的にexport宣言されている名前空間の定義の中にある宣言
- エクスポートされる宣言を含む名前空間の定義
export
ブロックの中にある宣言
宣言は再宣言できるが、再宣言によってエクスポートの有無が変わることはない。すなわち、
- エクスポートされている宣言の再宣言は、暗黙的にエクスポートされる。
- エクスポートされていない宣言の再宣言をエクスポートすることはできない。
エクスポートされる宣言が導入する名前は、そのモジュールからエクスポートされる。
C++20では、新たにモジュールリンケージが追加された。
- 名前のあるモジュールに属していてエクスポートしていない名前は、モジュールリンケージを持つ。
- エクスポートしている名前は外部リンケージを持つ。
- モジュールリンケージを持つ名前は、同一モジュール内で参照できる。
モジュールインポート宣言は次のようになる:
import lib; // libのインポート
モジュールインポート宣言は、モジュールのインターフェースユニットをインポートする。
インポートされた翻訳単位でエクスポートされている名前は、インポート宣言を記述した翻訳単位において可視(visible)となる。 名前が可視であるとき、かつそのときに限り、名前は名前探索の候補となる。
マクロやusing namespace
はエクスポートできないので、インポートによって取り込まれることはない。
ヘッダーファイル中での using namespace
はしばしば避けられるが、モジュールでは問題なく使うことができる。
インポート宣言もエクスポートできる。これを再エクスポートという。
export import lib; // libの再エクスポート
モジュールをインポートすると、そのモジュールが再エクスポートしているモジュールも同時にインポートする。
翻訳単位がモジュールユニットUにインターフェース依存(interface dependency)を持つとは、次のことをいう:
- Uをインポートするモジュールインポート宣言か、Uを暗黙的にインポートするモジュール宣言を含む
- または、Uにインターフェース依存を持つモジュールユニットに対してインターフェース依存を持つ(推移律)
翻訳単位は、自分自身に対してインターフェース依存を持ってはならない(インターフェース依存関係は循環しない)。
C++20では、翻訳単位と宣言に対して到達可能という用語を使うようになった。
翻訳単位Uがプログラムの点Pから必然的に到達可能(necessarily reachable) とは、次のことをいう:
- Uがモジュールインターフェースユニットであり、点Pを含む翻訳単位が点Pに先立ってUにインターフェース依存を持っている
- または、点Pを含む翻訳単位が点Pに先立ってUをインポートしている
点Pから到達可能(reachable)な翻訳単位とは、次のものをいう:
- 点Pから必然的に到達可能な翻訳単位
- その他、点Pを含む翻訳単位がインターフェース依存を持つ翻訳単位であって、処理系が規定するもの
宣言Dが点Pから到達可能とは、次のことをいう
- DがPと同じ翻訳単位にあり、Pに先立って宣言されている
- または、DがPから到達可能な翻訳単位にあって、破棄(discard)されておらず、プライベートモジュールフラグメント内にもない
C++20までは到達可能という用語はなかったが、前者の条件を満たす宣言だけが参照できていた。 宣言が到達可能であるとき、かつそのときに限り、宣言の意味論的な性質(semantic property)を利用できる。
例えば、クラス定義はクラスの完全性という性質を持っている。クラス定義が到達可能であるときそのクラスは完全である。
エクスポートの有無とは関係なく、モジュールをインポートしただけでインターフェース依存が発生し、そのモジュールインターフェースユニットおよびその中の宣言へ到達可能となる。
モジュールは分割することができる。分割したモジュールをモジュールパーティションという。
モジュールパーティションを宣言する構文は以下のようになる:
export(opt) module モジュール名:モジュールパーティション名 属性(opt);
- モジュールパーティション名の書式は、モジュール名と同じである。
export
がある場合をモジュールインターフェースパーティション、ない場合をモジュール実装パーティションという。
export module lib:part; // libモジュールのモジュールインターフェースパーティションpart
module lib:internal; // libモジュールのモジュール実装パーティションinternal
モジュールパーティションは基本的に別のモジュールと考えてよいが、以下の点で異なる:
- 主となるモジュールが異なる場合はインポートできない。
- 外部へ公開するには、モジュールインターフェースから再エクスポートする。
- モジュールの利用者にパーティションの存在を意識させてはいけない。
- インポート宣言にはモジュールパーティション名だけを書く。
- インポートするとエクスポートしていない宣言も見えるようになる。
- ただし、再エクスポートはできない。
主となるモジュールのインターフェースとパーティションを区別する場合は、プライマリーモジュールインターフェースユニットという事がある。
// P1103R3より引用
// 翻訳単位1
export module A;
export import :Foo;
export int baz();
// 翻訳単位2
export module A:Foo;
import :Internals;
export int foo() { return 2 * (bar() + 1); }
// 翻訳単位3
module A:Internals;
int bar();
// 翻訳単位4
module A;
import :Internals;
int bar() { return baz() - 10; }
int baz() { return 30; }
このモジュールAは4つの翻訳単位からなる。上から順に、
- (プライマリー)モジュールインターフェースユニット
- モジュールインターフェースパーティション
:Foo
- モジュール実装パーティション
:Internals
- モジュール実装ユニット
同じトークン列であれば再定義しても良いというODRの例外は、その定義が名前のあるモジュールに属する場合は適用されない。
この例外はヘッダーファイルにクラス定義などを書いてインクルードした際にODR違反にならないための規定である。 モジュールを定義する場合はヘッダーファイルは使わないから、実質的な影響はない。
モジュール内ではODRの例外が働かないため、複数のモジュールが#include
で同じ宣言を取り込んだ場合、ODR違反となってしまう。
このため、モジュール内では基本的に#include
を使用することはできない。
しかし、それでは従来のライブラリが利用できないため、モジュール宣言の前にグローバルモジュールの実装を書けるようになっている。これをグローバルモジュールフラグメントという。
module; // グローバルモジュールフラグメントの宣言
#include "lib.h" // "lib.h"中の宣言はグローバルモジュールに属する(ODRの例外が有効)。
export module foo; // モジュールの宣言(この上の行までがグローバルモジュールフラグメント)
#include "lib.h" // "lib.h"中の宣言がモジュールfooに含まれてしまう(ODRの例外なし = ODR違反の可能性大)。
モジュールユニットでの#include
はグローバルモジュールフラグメントで行うべきである。
グローバルモジュールフラグメントにはプリプロセッサディレクティブのみ記述できる。 翻訳フェーズ4(=プリプロセッサ実行時)以前の段階でそれ以外の記述がある場合は、エラーとなる。
グローバルモジュールフラグメント内の宣言は、後続のモジュールに属する宣言から参照されていない場合は、破棄(discard)される。
一部のヘッダーファイルは、モジュールとしてインポートすることができる。この機能およびヘッダーファイルから生成される翻訳単位をヘッダーユニットという。
import <foo.h>; // foo.hをヘッダーユニットとしてインポート
ただし、インポートできるヘッダーファイル(インポータブルヘッダー)は以下のものに限られる。
- C++ライブラリヘッダー(C++標準ライブラリヘッダーのうち、C言語標準ライブラリヘッダーに由来するもの(
<cstdio>
など)以外) - その他、処理系定義のヘッダー
ヘッダーユニットをインポートしてもその内容が展開されることはないが、#include
とほぼ同じ効果が得られる(そのようなヘッダーファイルだけがインポータブルヘッダーに指定されるともいえる)。
プリプロセッサは、インポータブルヘッダーに対する#include
ディレクティブをimport
宣言に置換できる。ただし、実際に行われるかは処理系定義である。
ヘッダーユニットはインポートしたときの効果を#include
と近くするために、普通のモジュールとは異なる性質をもつ。
- ヘッダーユニットはモジュール宣言を持てない。
- ヘッダーユニット内の宣言はすべてグローバルモジュールに属し、名前を導入する宣言ならば暗黙的にエクスポートされる。
- ヘッダーユニットをインポートすると、ヘッダーユニット内のマクロが使えるようになる。
// P1103R3より引用
// a.h
#define X 123 // #1
#define Y 45 // #2
#define Z a // #3
#undef X // a.hではここで#1が無効になる
// b.h
import "a.h"; // b.hではここで#1, #2, #3が定義され、#1が無効になる
#define X 456 // OK: #1で定義したXはすでに無効
#define Y 6 // エラー: #2で定義したYが有効
// c.h
#define Y 45 // #4
#define Z c // #5
// d.h
import "a.h"; // d.hではここで#1, #2, #3が定義され、#1が無効になる
import "c.h"; // d.hではここで#4, #5が定義される
int a = Y; // OK: #4は#2と同じ
int c = Z; // エラー: #5は#3を異なる値で再定義している
ヘッダーユニットは再エクスポートできるが、ヘッダーユニットを間接的にインポートした場合はマクロはインポートされない。
// lib.h
inline int f(){ return NUM; }
#define NUM 1000
// lib_mod.cpp
// lib.h中の宣言をすべてエクスポートするモジュールlib
export module lib;
export import "lib.h";
// main.cpp
import lib;
int main() {
1 + f(); // OK
1 + NUM; // エラー: マクロは再エクスポートしても引き継がれない
}
ヘッダーユニットをインポートすると以下のことが起こる。
- ヘッダーファイルを翻訳フェーズ7までコンパイルし、その翻訳単位(ヘッダーユニット)をインポートする。
- さらに、ヘッダーファイルの翻訳フェーズ4終了時点で定義されていたマクロがインポート宣言の直後で宣言される。この処理はプリプロセッサで行われる。
ヘッダーファイルが新たな翻訳単位としてコンパイルされる点が従来の #include
とは異なる。
ヘッダーユニット内のマクロはインポートできるが、逆は起こらない。すなわち、import
を書いた翻訳単位におけるプリプロセッサの状態がヘッダーユニット内に影響を与えることはない。
単にヘッダーファイルをインクルードしたいだけであれば、グローバルモジュールフラグメント内で行えば問題は無い。
しかし、処理系はヘッダーファイルをインポータブルヘッダーに指定することで、それらに対する #include
を import
に置き換え、
コンパイルを速くすることができる。
例えば、C++20では標準ライブラリは従来通りヘッダーファイルで提供されるが、C++ライブラリヘッダーはインポータブルヘッダーでもある。処理系はこれらをコンパイル済みモジュールとして提供するかもしれない(ヘッダーファイルからヘッダーユニットを生成する手順を事前に行っておくことは禁止されていない)。
そのような処理系では、C++20としてコンパイルするだけで従来のコードでも恩恵を得ることができる。
プログラムのビルドは規格の範囲外なので、ここでは一般論を述べる。
モジュールをコンパイルすると、何らかの中間表現(コンパイル済みモジュール)が保存される。
- テンプレートはテンプレートのまま(実体化することなく)エクスポートできるので、中間表現は機械語ではなく、いわゆるプリコンパイルドヘッダーと似たようなものにならざるを得ない。
- モジュールAをインポートするプログラムをコンパイルするには、モジュールAのコンパイル済みモジュールが存在しなければならない。
- モジュールAをインポートするプログラムをリンクするには、モジュールAに関するモジュールユニットから生成されるオブジェクトファイル、ライブラリなどを別途リンクしなければならない。
プリプロセッサによるインクルードは、ヘッダーファイルの内容をその場に展開する。 これには次のような問題が指摘されてきた。
- コンパイル時間が長くなる
- ヘッダーファイルの内容が再帰的に展開され、プログラムが長くなる(Hello worldだけでも数万行に達する)
- さらに、展開が翻訳単位ごとに行われるので、全体で見ると同じヘッダーファイルが何度も解析される
- プリプロセッサの状態により、インクルードの結果が変わってしまう
- インクルードの順番によってエラーが起きることがあった。
- ヘッダーファイル内の記述の影響を受けすぎる
- 影響が大きいため、ヘッダーファイル内に書くことがためらわれる記述があった。
using namespace
やマクロ(例えばWindowsにおけるmax
)など。
モジュールは、以上のような問題のないプログラム分割の仕組みとして導入された。
- P1103R3 Merging Modules
- P1502R1
Standard library header units for C++20
C++ライブラリヘッダーはインポータブルヘッダーとなった。 - P1703R1 Recognizing Header Unit Imports Requires Full Preprocessing
ヘッダーユニットのインポート宣言について、書き方が#include
と同程度まで制限された。 - P1766R1 Mitigating minor modules maladies
- P1811R0
Relaxing redefinition restrictions for re-exportation robustness
同時に到達可能とならなければODR違反にならないという仕様が削除された。また、インポータブルヘッダーの#include
をimport
に置き換えるかは処理系定義となった。