勝手に作るCMake入門の2本目です。
前回の記事ではCMakeのhello_worldプロジェクトを作成しました。
今回はそこから少し踏み込んで、プロジェクトを静的ライブラリとそれに依存する実行ファイルという2段階に階層化する方法を見ていきます。
全体のもくじ
- 基本的な使い方
- プロジェクトの階層化【今ここ】
- プロジェクトの設定
- 外部ライブラリを利用する
この記事のもくじ
スポンサーリンク
4. プロジェクトを階層化させる
プロジェクトの階層化は、どのプログラミング言語でも取り上げられるトピックです。 階層化は、汎用的な処理をモジュール化して他のプロジェクトで再利用できるようにしたり、コード間の依存関係をはっきりさせてメンテナンス性を高めたりするためのテクニックです。
C++ではプロジェクトを階層化するための仕組みとして静的リンクライブラリ, 動的リンクライブラリ(共有ライブラリ), 動的読み込み(動的ロード)の3つが用意されているのでした。 このあたりは以前の記事で紹介しています。
今回の記事では、uftree
という静的ライブラリの中にUnionFindTree
というクラスを作り、いくつかの関数を実装してみます。
そして、それを利用する実行ファイルmain_app
を作成してみます。
一応、プロジェクトの完成形をGitHubに上げておきますので必要に応じて参照してください(https://github.com/kamino410/blog_cmake_tutorial/tree/master/step2)。
4.1. 静的ライブラリを実装する
まずは適当にプロジェクトディレクトリを作成し、その中にuftree
というサブディレクトリを作成します。
今回は、お行儀よくinclude
ディレクトリとsrc
ディレクトリを分けることにしましょう。
以下のようなディレクトリ構成でuftree.hpp
, uftree.cpp
, CMakeLists.txt
を作成してください。
<プロジェクトディレクトリ> |- uftree/ |- include/ |- uftree.hpp |- src/ |- uftree.cpp |- CMakeLists.txt
次に、uftree.hpp
とuftree.cpp
の中にUnionFindTree
クラスを実装します。
以下のソースコードをコピー&ペーストしてください。
./uftree/include/uftree.hpp
のソースコード
#pragma once #include <vector> class UnionFindTree { private: const int N; // 要素数 std::vector<int> par; // 各ノードの親のID(根ノードは自分を参照) std::vector<int> sizes; // 各ノードを根とする木のサイズ(根でないノードには無関係) public: UnionFindTree(int n); int find(int x); void unite(int x, int y); bool same(int x, int y); void show(); int size(int x); };
./uftree/src/uftree.cpp
のソースコード
#include "../include/uftree.hpp" #include <functional> #include <iostream> UnionFindTree::UnionFindTree(int n) : par(n), sizes(n, 1), N(n) { for (int i = 0; i < n; i++) par[i] = i; } // 要素xが属するグループの根ノードのIDを見つける int UnionFindTree::find(int x) { if (par[x] == x) return x; else return par[x] = find(par[x]); } // 要素x, yが属するグループ同士を統合する void UnionFindTree::unite(int x, int y) { x = find(x); y = find(y); if (x == y) return; if (sizes[x] < sizes[y]) { par[x] = y; sizes[y] += sizes[x]; } else { par[y] = x; sizes[x] += sizes[y]; } } // グループのリストを表示する // (IDが小さい順に表示される, O(n^2)なので最速の実装ではない) void UnionFindTree::show() { std::cout << "Groups : " << std::endl; std::function<void(int)> f = [&](int x) { std::cout << x << ','; for (int y = 0; y < N; y++) { if (par[y] == x && y != x) f(y); } }; for (int i = 0; i < N; i++) { if (par[i] == i) { f(i); std::cout << std::endl; } } } // 要素x, yが同じグループに属するかどうか bool UnionFindTree::same(int x, int y) { return find(x) == find(y); } // 要素xが所属するグループに含まれる要素の数 int UnionFindTree::size(int x) { return sizes[find(x)]; }
ちなみにUnionFind木は素集合(1つの要素が1つの集合だけに所属する系)を管理しつつ、
- ある2つの要素が所属するグループ同士を結合させる
- 2つの要素が同じグループに所属するか判定する
の2つの操作を高速に行うためのデータ構造です。 アルゴリズムの教科書では割と冒頭の方で紹介されているやつですね。 今回は要素数Nを最初に固定してしまうタイプの実装です。 詳細は他の記事を参照してください。
4.2. ライブラリのCMakeLists.txtを書く
ソースコードを準備したので、次は./uftree/CMakeLists.txt
にuftreeライブラリ用の設定を書いていきましょう。
cmake_minimum_required(VERSION 3.1) project(uftree_lib VERSION 1.0.0 DESCRIPTION "Union-Find tree library" # URL関係のライブラリをインストールしないと動かないことがあるので # コメントアウトしておきます(2019/12/02 修正) # HOMEPAGE_URL "https://example.com" LANGUAGES CXX) add_library(uftree STATIC ./src/uftree.cpp) target_compile_features(uftree PRIVATE cxx_std_11) target_include_directories(uftree INTERFACE ./include) set_target_properties(uftree PROPERTIES VERSION ${PROJECT_VERSION})
今回も解説すべきことが多いです。
1〜6行目 CMakeのバージョン指定とプロジェクトの設定
1行目から6行目は前回と同じく、CMakeのバージョン指定とプロジェクト名の指定です。
project
はこんな感じにVERSION, DESCRIPTION, HOMEPAGE_URLを指定することもできます。
次の項で説明するように、このCMakeスクリプトはadd_subdirectory
コマンドを使ってmain_app用のCMakeLists.txtに取り込むので本来は必要ありません。
しかし、もしuftreeライブラリを単体でビルドすることになった場合はこの5行が必要になるので、書いておいても損はしないと思います。
7行目 ライブラリの作成
7行目は前回説明したadd_executable
と同じ要領で、uftree
というライブラリをビルド対象として宣言しています。
指定できるライブラリの種類にはSTATIC
(静的リンクライブラリ), SHARED
(動的リンクライブラリ), MODULE
(動的ロード)がありますが、ここでは静的リンクライブラリをビルドするためにSTATIC
を指定しています。
なお、ここではライブラリの種類を明示しないままにしておいてConfigureのときに指定する、という方法もあります。
気になる方はBUILD_SHARED_LIBS
というCMakeキャッシュ変数について調べてみてください。
キャッシュ変数の使い方は次回の記事で紹介します。
8行目 ビルドプロパティの指定
8行目では、作成したuftreeライブラリのビルドをC++11規格で実行するように指定しています。
UnionFindTree::show()
の中で、C++11から追加された仕様であるラムダ式を利用しているからです。
target_*
系のコマンドでは、ターゲット名の後にPUBLIC
, PRIVATE
, INTERFACE
のいずれかを指定します。
ターゲットが実行ファイルであるときはどれを選んでも関係ないのですが、ライブラリであるときはどれを選ぶのかによって挙動が以下のように変わります。
PUBLIC
: コマンドの内容が"自分自身"と"自分に依存するターゲット"に反映されるPRIVATE
: コマンドの内容が"自分自身"にのみ反映されるINTERFACE
: コマンドの内容が"自分に依存するターゲット"にのみ反映される
例えば、今回のサンプルではuftreeライブラリをmain_appから利用します。
つまり"自分自身"=uftreeで、"自分に依存するターゲット"=main_appです。
8行目のtarget_compile_features
はUnionFindTree::show()
の中身のための設定であり、main_appまでC++11規格でビルドする必要性はありません。
よってここではPRIVATE
を指定しています。
9行目 インクルードディレクトリの指定
9行目のtarget_include_directories
では、uftreeのインクルードディレクトリを指定しています。
今回の実装をよく見ていただくと、uftree.cpp
の中ではヘッダファイルを#include "../include/uftree.hpp"
のように相対パスで参照しています。
これは2つのファイルの相対位置を変えることはないだろうという理由からです。
そのため、ターゲットuftreeをビルドするときに追加でインクルードディレクトリを指定する必要はありません。
しかしuftreeを参照するmain_appのmain.cpp
では#include <uftree.hpp>
のように参照したいところです。
なぜならプロジェクトを分離して運用することになった場合、必ずしも相対パス./uftree/include/uftree.hpp
にヘッダファイルが置かれるとは限らないからです。
このためには、uftreeに依存するターゲットにuftreeのインクルードディレクトリを教えてやる必要があります。
というわけで、uftree自身には必要ないけどuftreeに依存するターゲットにはインクルードディレクトリを追加したい、という状況なのでここではINTERFACE
を指定しています。
10行目 ライブラリのプロパティ設定
10行目のset_target_properties
では、プロジェクトに設定したバージョン番号をそのままライブラリのバージョン番号に反映しています。
後の章で解説しますがCMakeスクリプトには変数の概念があります。
そして、実は2~6行目でプロジェクトを設定したとき、暗黙的にPROJECT_VERSION
とmylib_VERSION
という変数に1.0.0
という値がセットされています。
変数に格納されている値は${PROJECT_VERSION}
のように${}
で囲うことで値を展開できるので、上のように書けばバージョン番号がそのままコピーされることになります。
わざわざバージョン番号を付けるのは説明の都合によるものなので、そんなこともできるんだなくらいの認識でOKです。
これでuftreeライブラリの実装は完了です。
4.3. メインプロジェクトを書く
次にmain_appを実装します。
<プロジェクトディレクトリ> |- uftree/ |- include/ |- uftree.hpp |- src/ |- uftree.cpp |- CMakeLists.txt |- main.cpp |- CMakeLists.txt
./main.cpp
のソースコード
#include <iostream> #include "uftree.hpp" #include "uftree/include/uftree.hpp" int main() { UnionFindTree uf(5); // 0~4の5つの要素について uf.unite(0, 2); // 0と2は同じグループ uf.unite(2, 4); // 2と4は同じグループ uf.unite(1, 3); // 1と3は同じグループ // 1と2は同じグループ? std::cout << "same(1, 2) : " << (uf.same(1, 2) ? "True" : "False") << std::endl; // 3が所属するグループのメンバーの数は? std::cout << "size(3) : " << uf.size(3) << std::endl; // グループのリストを表示 uf.show(); return 0; }
同じく./CMakeLists.txt
を書きます。
cmake_minimum_required(VERSION 3.1) project(subdirectory_sample CXX) add_subdirectory(./uftree) add_executable(main_app main.cpp) target_link_libraries(main_app uftree)
新しく増えたのは3行目と5行目ですね。
3行目はadd_subdirectory
コマンドを使って、先ほど書いた./uftree/CMakeLists.txt
を取り込んでいます。
気分としては関数呼び出しのようなもので、3行目のタイミングで./uftree/CMakeLists.txt
の中身が実行され、その後に4,5行目が実行されます。
5行目はmain_appがuftreeライブラリに依存していることを宣言しています。
この行のおかげで必ずuftree->main_appの順でビルドが行われ、main_appのビルド時にuftreeがリンクされるようになります。
また先ほどtarget_include_directories(uftree INTERFACE ...)
コマンドを書いたので、main_appのコンパイル時にuftreeのインクルードディレクトリがインクルードされるようになります。
これでmain_appの実装も完了です。
4.4. ビルドする
前回と同じようにビルドしてみましょう。
cd <プロジェクトディレクトリ> mkdir build cd build cmake .. cmake --build .
以下はmacOS+Makefileでの実行結果ですが、uftreeとmain_appが別々にビルドされていることがわかります。
Scanning dependencies of target uftree [ 25%] Building CXX object uftree/CMakeFiles/uftree.dir/src/uftree.cpp.o [ 50%] Linking CXX static library libuftree.a [ 50%] Built target uftree Scanning dependencies of target main_app [ 75%] Building CXX object CMakeFiles/main_app.dir/main.cpp.o [100%] Linking CXX executable main_app [100%] Built target main_app\
ビルド完了後、buildディレクトリの中にuftreeのライブラリファイルとmain_appの実行ファイルが作成されているはずです。
作成されたmain_appを実行してみましょう。
Visual Studioを使っているなら./build/x64/Debug/main_app.exe
、XCodeなら./build/Debug/main_app
あたりに作成されていると思います。
./main_app
same(1, 2) : False size(3) : 2 Groups : 0,2,4, 1,3,
ちゃんとmain_appがuftreeライブラリの機能を利用できていることがわかります。
以上、CMakeでプロジェクトを階層化する方法を紹介しました。
この記事ではuftreeを静的ライブラリとしましたが、もちろん動的ライブラリにすることもできます。 ただ、動的ライブラリは実行ファイルが発見できる場所に配置する必要があります。 このあたりの話は、CMakeプロジェクトのデプロイ(インストール)と併せて紹介する必要があるので、今回は省略します。
次の記事はキャッシュ変数を利用したプロジェクト設定の話です⇒勝手に作るCMake入門 その3 プロジェクトの設定
もしよければ↓の☆を1クリックお願いします!