かみのメモ

コンピュータビジョン・プログラムな話題中心の勉強メモ(記事一覧は https://kamino.hatenablog.com/archive へ)

勝手に作るCMake入門 その2 プロジェクトの階層化

勝手に作るCMake入門の2本目です。

前回の記事ではCMakeのhello_worldプロジェクトを作成しました。

今回はそこから少し踏み込んで、プロジェクトを静的ライブラリとそれに依存する実行ファイルという2段階に階層化する方法を見ていきます。

全体のもくじ

  1. 基本的な使い方
  2. プロジェクトの階層化【今ここ】
  3. プロジェクトの設定
  4. 外部ライブラリを利用する

この記事のもくじ

4. プロジェクトを階層化させる

プロジェクトの階層化はどのプログラミング言語でも取り上げられるトピックです。 階層化は、汎用的な処理をモジュール化して他のプロジェクトで再利用できるようにしたり、コード間の依存関係をはっきりさせてメンテナンス性を高めたりするためのテクニックです。

C++ではプロジェクトを階層化するための仕組みとして静的リンクライブラリ, 動的リンクライブラリ(共有ライブラリ), 動的読み込み(動的ロード)の3つが用意されているのでした。 このあたりは以前の記事で紹介しています。

kamino.hatenablog.com

この記事では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.hppuftree.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つの集合だけに所属する系)を管理しつつ、

  1. ある2つの要素が所属するグループ同士を結合させる
  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"
    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_featuresUnionFindTree::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_VERSIONmylib_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.exeXCodeなら./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クリックお願いします!