CMakeでsrcとappを分けるときに設定しておくべきこと

CMakeで小さいサンプルを書くくらいなら、ルートの CMakeLists.txt に全部並べてもそこまで困りません。

ただ、main.cpp の横に共通処理が増えてきたり、あとからテストを足したくなったりすると、だんだん見通しが悪くなります。srcapp を分けたくなるのはこのあたりから。

調べた範囲では、以下のように役割を分けておく形が扱いやすいようです。

  • src 側でライブラリを作る
  • app 側で実行ファイルを作る
  • ルートから add_subdirectory() で両方を読む
  • app から src のライブラリへ target_link_libraries() する

先に構成だけ見ると、こんな形です。

ディレクトリ構成

my_project/
├── CMakeLists.txt
├── app/
│   ├── CMakeLists.txt
│   └── main.cpp
└── src/
    ├── CMakeLists.txt
    ├── hello.cpp
    └── hello.hpp

src は共通処理の置き場。app は実行ファイルの置き場。

ここで src/hello.cppapp から直接参照するのではなく、src 側を一度 hello_lib というライブラリにしてからリンクします。

app/main.cpp
  -> hello_lib
      -> src/hello.cpp
      -> src/hello.hpp

この形にしておくと、あとで test や別の実行ファイルを足すときに hello_lib を使い回せます。

関連するCMake記事は CMakeシリーズ にまとめていきます。

ルートのCMakeLists.txt

ルートの CMakeLists.txt は、全体設定とディレクトリ追加だけにします。

add_subdirectory() は、指定したディレクトリにある CMakeLists.txt をビルドに追加するコマンドです。

https://cmake.org/cmake/help/latest/command/add_subdirectory.html

cmake_minimum_required(VERSION 3.20)
project(MyProject LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_subdirectory(src)
add_subdirectory(app)

順番は src が先です。

app 側で hello_lib をリンクするので、add_subdirectory(app) の時点で hello_lib がすでに定義されている必要があります。ここを逆にすると、hello_lib が見つからない状態になります。

src側でライブラリを作る

src/CMakeLists.txt では、共通処理をライブラリとして定義します。

add_library() はライブラリターゲットを作るためのコマンドです。

https://cmake.org/cmake/help/latest/command/add_library.html

add_library(hello_lib
  hello.cpp
)

target_include_directories(hello_lib
  PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}
)

hello_lib はこのプロジェクト内で使うライブラリ名です。名前は何でもよいですが、app 側からも参照するので、何のライブラリか分かる名前にしておくと後で追いやすくなります。

ここで target_include_directories()PUBLIC を付けています。target_include_directories() は、ターゲットごとのincludeパスを設定するコマンドです。

https://cmake.org/cmake/help/latest/command/target_include_directories.html

hello.hpphello_lib の中でも使い、hello_lib を使う側の app/main.cpp からも include します。そのため、ここでは PRIVATE ではなく PUBLIC としています。

ざっくり使い分けるなら、このくらいで見ています。

指定 使う場面
PRIVATE そのターゲットの中だけで必要
PUBLIC そのターゲット自身にも、使う側にも必要
INTERFACE 使う側にだけ伝えたい

最初は、「リンクした相手にも include パスを渡したいなら PUBLIC」くらいで見ておくと整理しやすいです。

app側で実行ファイルを作る

app/CMakeLists.txt は、実行ファイルを作って hello_lib をリンクします。

add_executable(my_app
  main.cpp
)

target_link_libraries(my_app
  PRIVATE
    hello_lib
)

target_link_libraries() は、ターゲット同士の依存関係を設定するコマンドです。

https://cmake.org/cmake/help/latest/command/target_link_libraries.html

my_app から見ると、必要なのは hello_lib だけです。

hello_lib 側で include パスを PUBLIC にしてあるので、app/main.cpp ではそのまま hello.hpp を include できます。

#include "hello.hpp"

int main() {
    hello();
}

ここで app 側に include_directories(../src) のような設定を書かなくてよいのがポイントです。依存関係は target_link_libraries() に寄せておく方が追いやすくなります。

サンプルコード

src/hello.hpp:

#pragma once

void hello();

src/hello.cpp:

#include "hello.hpp"

#include <iostream>

void hello() {
    std::cout << "Hello from hello_lib" << std::endl;
}

app/main.cpp:

#include "hello.hpp"

int main() {
    hello();
}

ビルドはルートディレクトリで行います。

cmake -S . -B build
cmake --build build

この例では、実行ファイルは次の場所に生成されます。

./build/app/my_app

避けたい書き方

appを先にadd_subdirectoryする

これは順番の問題です。

add_subdirectory(app)
add_subdirectory(src)

app の中で hello_lib を使っているなら、この順番だとまだ hello_lib が存在しません。

基本はこう。

add_subdirectory(src)
add_subdirectory(app)

include_directoriesを全体に効かせる

昔のサンプルだと、こういう書き方を見かけます。

include_directories(src)

小さいうちは動きます。ただ、どのターゲットがどの include パスに依存しているのか分からなくなりやすくなります。

新しく書くなら、まずは target_include_directories() を使う形に寄せます。

appからsrcのcppを直接足す

これも動きます。

add_executable(my_app
  main.cpp
  ../src/hello.cpp
)

ただ、src を分けたのに app 側から中身を直接拾っていて、構成としては少し苦しくなります。

あとからテスト用の実行ファイルを作ると、また ../src/hello.cpp を足すことになります。src 側をライブラリにしておいて、必要なところからリンクする方が整理しやすいです。

この構成にする目安

全部のプロジェクトで最初から分ける必要はありません。

1ファイルの動作確認なら、ルートの CMakeLists.txtadd_executable() だけでも十分です。逆に、次のどれかに当てはまるなら srcapp を分ける目安になります。

  • 実行ファイルと共通処理を分けたい
  • テストターゲットを足す予定がある
  • 同じ処理を複数の実行ファイルから使いたい
  • includeパスをターゲット単位で管理したい

「あとからもう1つターゲットが増えそう」という段階なら、分けておく判断になります。

まとめ

srcapp を分けるときは、ディレクトリを分けるだけでなく、CMake上でもターゲットを分けておくと扱いやすくなります。

  • ルートで add_subdirectory(src)add_subdirectory(app) を呼ぶ
  • src 側で add_library() を使う
  • app 側で target_link_libraries() する
  • includeパスは target_include_directories() でターゲットに持たせる

ファイルパスで無理につなぐより、ターゲット同士をリンクする。CMakeではこの形にしておく方が、あとから構成を変えやすいです。

参考