C++20 モジュールとCMakeLists.txtの書き方
C++20で言語機能にモジュールが追加された。
時は流れ、そろそろヘッダーをやめてモジュールを書きたい。
module; // これはグローバルモジュールフラグメント。
// ここにプリプロセスで処理するマクロ等を記述する。
#include <iostream>
export module HelloWorld; // エクスポートを書かなければ他のファイルから利用できない。
// ここからHelloWorldという名前のモジュールとなる。
export void HelloWorld() // 関数宣言もエクスポートを書かなければ他のファイルから利用できない。
{
std::cout << "Hello, world!" << std::endl;
}
// 呼び出し側は、こう書く。
import HelloWorld; // モジュールをインポートする構文。インクルードからの移行先。
int main()
{
HelloWorld(); // インポートしたモジュールに含まれる関数を呼び出す。
return 0;
}
うん。いい感じだ。
クラスも書きたい。
module;
#include <iostream>
#include <string>
export module Greeting;
export class Greeting
{
public:
Greeting(std::string sentence)
:m_sentence{sentence}
{}
void Greet() // クラスにエクスポートを書けば、メソッドにエクスポートはいらないのだ。
{
std::cout << m_sentence << std::endl;
}
private:
std::string m_sentence;
};
import HelloWorld;
int main()
{
Greeting greeting("Hello, world!");
greeting.Greet();
// もちろんポインタでも呼び出せる
Greeting *pGreeting = new Greeting("Hello, pointer!")
pGreeting->Greet();
}
よし、その調子だ。
構造体はどうだろう?
クラスと違う点は、アクセス修飾子を書かなかったときのアクセシビリティだったな。
そのアクセシビリティはパブリックだ。
module;
#include <string>
export module Greetings;
export struct Greetings
{
std::string moning;
std::string afternoon;
std::string night;
};
import Greeting;
import Greetings;
int main()
{
Greetings greetings{ "Good moning!", "Good afternoon!", "Good night!"};
Greeting moning(greetings.moning);
moning.Greet();
Greeting afternoon(greetings.afternoon);
afternoon.Greet();
Greeting night(greetings.night);
night.Greet();
}
アクセシビリティしか違いがないから、書けて当然だったな。
そういえば、さっきからグローバル名前空間に定義してばかりだ。
もしもライブラリを作成して頒布するなら専用の名前空間に定義したい。
module;
#include <string>
export module Greeting;
namespace myLibrary // 名前空間にエクスポートは書かずとも他のファイルから利用できる。
{
export class Greeting
{
public:
Greeting(std::string sentence)
:m_sentence{sentence}
{}
void Greet()
{
std::cout << m_sentence << std::endl;
}
private:
std::string m_sentence;
};
}
export module Greetings; // ひとつのファイルに複数のモジュールを含められる。
namespace myLibrary
{
export struct Greetings
{
using namespace std;
string moning;
string afternoon;
string night;
};
}
import Greetings;
import Greeting;
int main()
{
// 名前空間へアクセスするにはスコープ解決演算子が必要である。
myLibrary::Greetings greetings{"Hello", "Hello", "Hello"};
// スコープ解決演算子の左側に何もないならグローバル名前空間の明示だ。
::myLibrary::Greeting greeting(greetings.moning);
greeting.Greet();
}
よし!コーディングはあらかたできたから、ビルドしよう。
CMakeを利用したい。
cmake_minimum_required(VERSION 3.28)
project(myModule)
add_library(moduleLib static) # モジュールの提供者はライブラリとしてビルドする。今回は静的。
target_compile_features(moduleLib PUBLIC cxx_std_20) # cxx_std_20は、バージョン3.12で追加された。
target_source(moduleLib PUBLIC
FILE_SET CXX_MODULES # この"CXX_MODULES"が、かなめ。3.28で追加された。
FILES
Greeting.ixx)
add_execute(myexe) # これの引数にソースファイルを指定しても良いが、今回はそうしない。
# モジュールの利用者は、モジュールをリンクする。
target_link_libraries(myexe private
moduleLib)
target_source(myexe private
main.cpp)
これで、めでたしめでたし。ビルドできました。
ん? おや、メソッドの定義と実装のファイルを分けたいだって?
module;
#include <string>
export module Greeting;
namespace myLibrary // 名前空間にエクスポートは書かずとも他のファイルから利用できる。
{
export class Greeting
{
public:
Greeting(std::string);
void Greet();
private:
std::string m_sentence;
};
}
module;
#include <string> // メソッドの定義と宣言のファイルのどちらともに書かなければならない。
module Greeting; // 実装を書くファイルはエクスポートを書かない。
namespace myLibrary // 定義は、宣言と同じ名前空間でなければならない。
{
Greeting::Greeting(std::string sentence)
:m_sentence{sentence}
{}
}
void myLibrary::Greeting::Greet() // このようにスコープ解決演算子で名前空間をそろえても良い
{
std::cout << m_sentence << std::endl;
}
cmake_minimum_required(VERSION 3.28)
project(myModule)
add_library(moduleLib static)
target_compile_features(moduleLib PUBLIC cxx_std_20)
target_source(moduleLib PUBLIC
FILE_SET CXX_MODULES
FILES
Greeting.ixx)
# 宣言のファイルとは別個に定義のファイルを記述する。"FILE_SET CXX_MODULES"は、いらない。
target_source(moduleLib PRIVATE
Greeting.cpp)
add_execute(myexe)
target_link_libraries(myexe PRIVATE
moduleLib)
target_source(myexe PRIVATE
main.cpp)
ふむ。宣言と定義を分けて、モジュールをライブラリとしてビルドすれば再利用性が高そうだ。
だがライブラリにしないなら、こう書ける。
cmake_minimum_required(VERSION 3.28)
project(myModule)
add_execute(myexe main.cpp) # ここに"main.cpp"を記述して目立たせている。ほかに意味はない。
target_compile_features(myexe PRIVATE cxx_std_20)
target_source(myexe
FILE_SET CXX_MODULES
FILES
Greeting.ixx)
target_source(myexe PRIVATE
Greeting.cpp)
解説は以上となります。ご閲覧ありがとうございました。
※拡張子について
モジュールのファイルの拡張子は".ixx"または".cppm"が通例となっているようです。
その2つであれば、Visual Studio 2022のソリューションエクスプローラーでアイコンが専用のものになります。
ですが正しくFILE_SET CXX_MODULES
を記述していれば、拡張子は何でもいいです。
今回は、せっかくなので通例に従ってみました。
.ixxの"xx"の意味はC++の"++"を傾けたんでしょうかね?
CMakeもそうしてるし。CXX_で始まる変数多いもん。
追記:
.ixx
と.cppm
は、コンパイラが拡張子によって動作を変えてモジュールインターフェース(モジュール宣言)のファイルだと認識します。モジュール定義のファイルの拡張子はcpp
が良さそう。
Discussion