使用CMake模块化项目
# 使用CMake模块化项目
该笔记参考BiliBili-【公开课】现代CMake模块化项目管理指南【C/C++】 (opens new window)
# 推荐的目录组织结构
项目名/include/项目名/模块名.h
项目名/src/模块名.cpp
如下是一个例子:
.
├── CMakeLists.txt
├── biology
│ ├── CMakeLists.txt
│ ├── include
│ │ └── biology
│ │ ├── Animal.h
│ │ └── Carer.h
│ └── src
│ ├── Animal.cpp
│ └── Carer.cpp
├── cmake
│ └── MyUsefulFuncs.cmake
└── pybmain
├── CMakeLists.txt
├── include
│ └── pybmain
│ └── myutils.h
└── src
└── main.cpp
头文件一般会在include
目录里再嵌套一个项目名,目的是为了 避免头文件命名冲突,例如:
#include <pybmain/myutils.h>
#include <biology/myutils.h>
// 若没有项目名,就会产生冲突
# 各文件推荐写法
CMakeLists.txt:
CMakeLists.txt中推荐使用
target_include_directories(项目名 PUBLIC include)
;不要使用
include_directories(include)
,这样会污染头文件空间
.c:
#include <项目名/模块名.h>
namespace 项目名 {
void 函数名() { 函数实现 }
}
.h:
#pragma once
namespace 项目名 {
void 函数名();
}
# 一、划分子项目
即使只有一个子项目,也建议你创建一个子目录,方便以后追加新的子项目;
子项目,例如
biology
和pybmain
,他们分别在各自的目录下有自己的CMakeLists.txt
。- 一般一个项目是可执行文件(比如这里的
pybmain
),另一个是库文件(比如这里的biology
) - 可执行文件是给用户使用的,一般只有交互的逻辑;而实际代码的实现逻辑一般都在库当中。
- 一般一个项目是可执行文件(比如这里的
# 二、根目录的CMakeLists.txt配置
cmake_minimum_required(VERSION 3.18)
if (NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake;${CMAKE_MODULE_PATH}")
project(CppCMakeDemo LANGUAGES CXX)
include(MyUsefulFuncs)
add_subdirectory(pybmain)
add_subdirectory(biology)
根项目的 CMakeLists.txt
中,设置默认的构建模式,设置统一的 C++ 版本等各种选项。然后通过 project
命令初始化根项目。
随后通过 add_subdirectory
把两个子项目 pybmain
和 biology
添加进来,这会调用 pybmain/CMakeLists.txt
和 biology/CMakeLists.txt
。
# 三、子项目的CMakeLists.txt配置
file(GLOB_RECURSE srcs CONFIGURE_DEPENDS src/*.cpp include/*.h)
add_library(biology STATIC ${srcs})
target_include_directories(biology PUBLIC include)
子项目中主要创建了静态库对象,通过GLOB_RECRUSE
批量添加位于src
和include
下的源码与头文件。
- 根项目的
CMakeLists.txt
负责处理全局有效的设定。 - 而子项目的
CMakeLists.txt
则仅考虑该子项目自身的设定,比如他的头文件目录,要链接的库等等。
# GLOB与GLOB_RECRUSE的区别
file (GLOB myvar CONFIGURE_DEPENDS src/*.cpp)
file (GLOB_RECURSE myvar CONFIGURE_DEPENDS src/*.cpp)
GLOB
只能查找到往下一级的文件,而GLOB_RECURSE
能查到嵌套的目录;- 添加
CONFIGURE_DEPENDS
选项会在你创建了新文件后,进行cmake --build
时自动检测目录是否更新,并更新变量myvar
的值,而无需手动重新运行cmake -B build
# 四、子项目的头文件
- 这里需要给
biology
库设置了头文件搜索路径include
。 - 由于子项目的
CMakeLists.txt
里指定的路径都是相对路径,所以这里指定的include
实际上是:根/biology/include
。
# 五、子项目的源文件
这里利用
file(...)
给biology
批量添加了src/*.cpp
下的全部源码文件。因为子项目的
CMakeLists.txt
里指定的路径都是相对路径,所以这里指定src
实际上是:根/biology/src
。
# 六、只有头文件,没有源文件的情况
有时我们会直接把实现直接写在头文件里,这时可以没有与之对应的源文件,只有一个头文件。
注意:在头文件里直接实现函数时,要加
static
或inline
关键字。(类与结构体可以不加)- 防止被重复定义
# 七、依赖其他模块但不解引用,则可以只前向声明不导入头文件
假如模块 Carer 的头文件
Carer.h
引用了其他模块中的 Animal 类,但是并没有解引用(如没有进行指针的解引用->
,仅做变量声明使用) Animal,只有源文件Carer.cpp
解引用了 Animal。- 那么这个头文件是不需要导入
Animal.h
的,只需要一个前置声明struct Animal
,只有实际调用了 Animal 成员函数的源文件需要导入 Animal.h。 - 好处:加快编译速度,防止循环引用。
- 那么这个头文件是不需要导入
# 八、依赖另一个子项目,则需要链接
让
pybmain
链接上biology
:target_link_libraries(pybmain PUBLIC biology)
由于
PUBLIC
属性具有传染性,根/biology/include
现在也加入pybmain
的头文件搜索路径了,因此 pybmain 里可以#include
到biology
的头文件。
# 九、CMake中的include
写
include(XXX)
,则他会在CMAKE_MODULE_PATH
这个列表中的所有路径下查找XXX.cmake
这个文件。- 因此在include前,首先需要把
XXX.cmake
文件的路径加到CMAKE_MODULE_PATH
中; CMAKE_MODULE_PATH
列表的每个值用;
分割。
- 因此在include前,首先需要把
这样你可以在
XXX.cmake
里写一些你常用的函数,宏,变量等
示例MyUsefulFuncs.cmake
内容如下:
macro (my_add_target name type)
# 用法: my_add_target(pybmain EXECUTABLE)
file(GLOB_RECURSE srcs CONFIGURE_DEPENDS src/*.cpp src/*.h)
if ("${type}" MATCHES "EXECUTABLE")
add_executable(${name} ${srcs})
else()
add_library(${name} ${type} ${srcs})
endif()
target_include_directories(${name} PUBLIC include)
endmacro()
set(SOME_USEFUL_GLOBAL_VAR ON)
set(ANOTHER_USEFUL_GLOBAL_VAR OFF)
- function-CMake (opens new window)和macro-CMake (opens new window)的区别可以详细查看文档
- 简单来说:
macro
相当于直接把代码粘贴过去,直接访问调用者的作用域。这里写的相对路径include
和src
,是基于调用者所在路径。 function
则是会创建一个闭包,优先访问定义者的作用域。这里写的相对路径include
和src
,则是基于定义者所在路径。include
和add_subdirectory
同样,前者相当于直接粘贴,直接访问调用者的作用域,后者则优先访问定义者的作用域。