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 触发重新扫描。

知识体系图

下面这张图总结了本章的核心逻辑,我画的时候特意把“列表操作”和“循环遍历”分开,方便你理解它们的关系:

CMake 列表与循环知识体系 列表 (List) list() 命令 增删改查 排序去重 查找拼接 foreach 循环 遍历列表 范围遍历 ZIP 遍历 实战:用列表管理源文件 + 循环批量处理 避免重复劳动,提升 CMakeLists.txt 的可维护性

避坑指南

最后,分享几个我踩过的坑:

  • 列表元素包含空格:如果文件名里有空格,比如 my file.cpp,一定要用引号包起来:"my file.cpp"。否则 CMake 会把它当成两个元素。
  • 空列表的处理:对一个空列表执行 list(GET ...) 会报错。我习惯先检查长度:if(len GREATER 0) 再取值。
  • 循环中修改列表:foreach 遍历时,如果你在循环体里用 list(APPEND ...) 修改同一个列表,行为是未定义的。我建议先复制一份再操作。

我的小技巧:调试列表内容时,用 message("列表内容: ${MY_LIST}") 打印出来看看。CMake 会把列表元素用分号隔开显示,一目了然。

好了,关于循环和列表就聊这么多。说白了,这两个工具就是让你从重复劳动中解脱出来的。下次你看到 CMakeLists.txt 里有一长串文件名,别犹豫,用列表和循环重构它吧。


公众号:蓝海资料掘金营,微信deep3321