24、循环与列表:foreach循环遍历列表,list()命令操作列表
各位同学,今天我们来聊聊 CMake 里两个非常实用的工具——foreach 循环和 list() 命令。说实话,我刚开始用 CMake 的时候,总觉得它就是个“高级的 makefile 生成器”,直到我接手了一个有几十个源文件的项目……手动一个一个写 add_executable?那简直是噩梦。后来我学会了用列表和循环,嗯,世界清净了。
为什么需要列表?
你想想看,一个稍微像样点的 C++ 项目,源文件少说十几个,多则上百个。如果你在 CMakeLists.txt 里一个一个写文件名,不仅累,还容易漏。更麻烦的是,哪天加个文件,你得改好几个地方。
列表就是用来解决这个问题的。说白了,它就是一个有序的字符串集合。你可以把文件名、路径、编译选项统统塞进去,然后统一处理。
核心思想:用列表管理“一组东西”,用循环批量处理它们。
foreach 循环:遍历列表的利器
先看一个最简单的例子。假设我有一个源文件列表,想逐个打印出来:
set(MY_SOURCES
main.cpp
utils.cpp
network.cpp
database.cpp
)
foreach(src ${MY_SOURCES})
message("当前处理: ${src}")
endforeach()
这段代码会输出四行,每行一个文件名。我在项目中经常用这个来调试——看看列表里到底有哪些东西,有没有拼写错误。
foreach 还有几种变体,我挑两个常用的说说:
- 范围遍历:
foreach(i RANGE 1 10)会生成 1 到 10 的数字,适合做循环次数固定的场景。 - ZIP 遍历:
foreach(a b IN ZIP_LISTS LIST1 LIST2)可以同时遍历两个列表,把对应位置的元素配对。这个我用的不多,但偶尔处理键值对时挺方便。
我的习惯:遍历列表时,尽量用 ${src} 而不是 src。前者是变量展开,后者是字符串字面量。我刚开始学的时候搞混过,结果循环里一直打印 "src" 这个单词……排查了半天。
list() 命令:列表的增删改查
光有循环还不够,你还得能操作列表。CMake 提供了 list() 命令,功能相当全面。我把它分成几类:
1. 获取长度和元素
set(MY_LIST a b c d)
list(LENGTH MY_LIST len) # len = 4
list(GET MY_LIST 0 first) # first = a
list(GET MY_LIST -1 last) # last = d
注意索引从 0 开始,负数表示从末尾倒数。这个和 Python 有点像,我用起来很顺手。
2. 添加和删除
list(APPEND MY_LIST e f) # 变成 a b c d e f
list(INSERT MY_LIST 0 z) # 变成 z a b c d e f
list(REMOVE_ITEM MY_LIST b) # 删除所有 b
list(REMOVE_AT MY_LIST 0) # 删除索引 0 的元素
list(POP_FRONT MY_LIST) # 弹出第一个元素
list(POP_BACK MY_LIST) # 弹出最后一个元素
我曾经在项目中遇到一个坑:REMOVE_ITEM 会删除所有匹配的元素,不是只删第一个。如果你只想删一个,得用 REMOVE_AT 配合索引。
3. 查找和排序
list(FIND MY_LIST "c" idx) # idx = 2,没找到返回 -1
list(SORT MY_LIST) # 按字母顺序排序
list(REVERSE MY_LIST) # 反转顺序
查找时注意,CMake 是大小写敏感的。"C" 和 "c" 是两个不同的东西。我吃过这个亏,后来养成了统一小写的习惯。
4. 去重和拼接
list(REMOVE_DUPLICATES MY_LIST) # 去掉重复项
list(JOIN MY_LIST "," joined) # 用逗号连接成字符串
JOIN 这个命令特别实用。比如你要生成编译器的 include 路径,用分号连接起来,直接传给 target_include_directories 就行。
实战:用列表管理源文件
好了,理论说完了,咱们来点实际的。假设你有一个项目,源文件分布在多个子目录里:
project/
├── src/
│ ├── main.cpp
│ ├── ui/
│ │ ├── window.cpp
│ │ └── button.cpp
│ └── core/
│ ├── engine.cpp
│ └── math.cpp
└── CMakeLists.txt
传统做法是:
add_executable(myapp
src/main.cpp
src/ui/window.cpp
src/ui/button.cpp
src/core/engine.cpp
src/core/math.cpp
)
这样写没问题,但不够灵活。如果我用列表加循环:
file(GLOB_RECURSE SOURCES "src/*.cpp")
list(SORT SOURCES)
add_executable(myapp ${SOURCES})
你看,一行 file(GLOB_RECURSE) 就把所有 .cpp 文件抓进来了。再用 list(SORT) 排个序,保证每次构建的顺序一致。
注意:用 GLOB_RECURSE 有个坑——CMake 不会自动检测新文件。如果你在 src 目录下新建了一个 .cpp,必须重新运行 cmake 才能识别。我建议在开发阶段用 GLOB,发布时还是手动列出文件,或者用 configure_file 触发重新扫描。
知识体系图
下面这张图总结了本章的核心逻辑,我画的时候特意把“列表操作”和“循环遍历”分开,方便你理解它们的关系:
避坑指南
最后,分享几个我踩过的坑:
- 列表元素包含空格:如果文件名里有空格,比如
my file.cpp,一定要用引号包起来:"my file.cpp"。否则 CMake 会把它当成两个元素。 - 空列表的处理:对一个空列表执行
list(GET ...)会报错。我习惯先检查长度:if(len GREATER 0)再取值。 - 循环中修改列表:foreach 遍历时,如果你在循环体里用
list(APPEND ...)修改同一个列表,行为是未定义的。我建议先复制一份再操作。
我的小技巧:调试列表内容时,用 message("列表内容: ${MY_LIST}") 打印出来看看。CMake 会把列表元素用分号隔开显示,一目了然。
好了,关于循环和列表就聊这么多。说白了,这两个工具就是让你从重复劳动中解脱出来的。下次你看到 CMakeLists.txt 里有一长串文件名,别犹豫,用列表和循环重构它吧。
公众号:蓝海资料掘金营,微信deep3321