简介:gflags-2.1.1 是 Google 开源命令行标志处理库的经典版本,面向需要在 C++ 项目中灵活定义、解析和管理运行参数的开发者,也适合仍在维护旧版 Caffe 项目的深度学习使用者。压缩包共 50 个文件、约 100KB,以 cc 源码、h 头文件、cmake 构建脚本和 txt/in 配置及说明文件为主,并含少量 html、css、sh 等文档与辅助脚本,便于阅读实现、集成编译和参考配置。已有 222 人学习下载。读者可从中了解标志注册、长短参数解析、配置文件读取、帮助信息输出等机制,并借鉴其与 Caffe 训练参数设置的结合方式;对需要动态控制程序配置、排查命令行参数问题的开发者具有实用参考价值。
1. 一个 2015 年的 flag 库,今天还被翻出来编
上周帮人看一个老推理工程,main里密密麻麻全是手写的strcmp(argv[i], "--gpu"),加一个参数要改三处。我让他把gflags-2.1.1的源码包解开,编出libgflags.so.2.1,半天时间几百行解析代码就没了。这个包是 C++ 命令行参数库的源码发行版,核心就一件事:用DEFINE_string/DEFINE_int32把参数定义成全局变量,启动时一次ParseCommandLineFlags完成绑定,顺带白送--help、--version、--flagfile和 bash 补全。
它真正的高频场景是老 Caffe 那套训练/推理工程——tools/caffe.cpp依赖它做参数分发,Makefile里硬编码-lgflags。想复现一套老流程,绕不开这个包。适合两类人:手上有一堆散装argv想收编的 C++ 开发者,和要给老工程重新编依赖、但被命名空间和 ABI 卡住的人。
2. 从源码到 libgflags.so.2.1:一次干净的 CMake 构建
2.1 为什么不直接用系统源里那份
系统源里的 gflags 大概率不是 2.1.1,命名空间、SONAME、CMake 包配置都可能和你手上的工程对不上。最典型的翻车是:老代码里写着google::ParseCommandLineFlags,你装了个新版,头文件里那个google命名空间没了,编译直接过不去;或者反过来,代码按新写法编译通过,运行期加载的却是系统里另一份老.so,符号对不上,报一串看不懂的 undefined symbol。
自己编的价值不在“新”,在于你能把三件事固定下来:装到哪个前缀、编成静态还是动态、命名空间到底叫gflags还是google。这三个变量任何一个跟着系统走,后面排查问题都要多花两小时。
2.2 配置、构建、安装
不要在源码根目录直接cmake .,会给源码树塞一堆CMakeCache.txt和CMakeFiles,下次想看CMakeLists.txt原始内容都费劲。独立 build 目录是基本习惯。
# 1) 解压后进目录 tar -xzf gflags-2.1.1.tar.gz cd gflags-2.1.1 # 2) 独立构建目录,别污染源码树 mkdir -p build && cd build # 3) 配置:前缀、构建类型、命名空间三件事显式写死 cmake .. \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX="$HOME/.local/gflags-2.1.1" \ -DBUILD_SHARED_LIBS=ON \ -DBUILD_TESTING=OFF \ -DINSTALL_HEADERS=ON \ -DGFLAGS_NAMESPACE=gflags # 4) 把缓存项打出来,确认上面这些真的生效了,而不是被默认值覆盖 cmake -LH . | grep -i -E 'gflags|install_headers|build_shared'第 3 步每个参数都不是摆设。CMAKE_BUILD_TYPE=Release决定-O2 -DNDEBUG,Debug编出来的库拿去跑训练会慢一截。CMAKE_INSTALL_PREFIX指到$HOME下是刻意的:不碰/usr/local,将来系统里再装一份也不会打架,卸载就是rm -rf。BUILD_SHARED_LIBS=ON出动态库,如果你的老工程是静态链接、要单文件发布,这里改OFF。BUILD_TESTING=OFF是为了省事——打开测试它会去找 GTest,机器上没有就直接在 configure 阶段报错,很多人第一次编就卡在这一步。GFLAGS_NAMESPACE是本文后面所有麻烦的源头,先记住它默认是gflags。
第 4 步是个小技巧,-LH会把所有非 advanced 的缓存变量连同注释打出来。不同小版本的选项名有细微差别,与其背文档,不如让 CMake 自己报菜名。
# 5) 编译安装 make -j"$(nproc)" make install # 6) 看产物,同时确认软链层级 ls -l "$HOME/.local/gflags-2.1.1/lib"make -j"$(nproc)"在小机器上可能把内存打满,编译中途被 OOM killer 干掉的现象是make: *** [xxx.o] Killed,看着像语法错误其实是内存。4G 以下的机器老实写-j2。
2.3 安装出来的是些什么文件
| 路径(相对安装前缀) | 内容 | 什么时候会碰到 |
|---|---|---|
include/gflags/gflags.h | 主头文件,DEFINE_*/DECLARE_*宏和全部 API | 业务代码 include |
include/gflags/gflags_declare.h | 只有DECLARE_*宏 | 想在头文件里声明 flag 又不想引入整个库 |
include/gflags/gflags_completions.h | 补全相关接口 | 要让--tab_completion_word生效 |
lib/libgflags.so.2.1.1及两级软链 | 真实动态库与libgflags.so.2.1、libgflags.so | -lgflags找的是.so,运行时找的是.so.2.1 |
lib/libgflags.a | 静态库 | 静态链接、单文件交付 |
lib/libgflags_nothreads.a等 | 不依赖线程的变体 | 嵌入式或不想引入 pthread |
lib/cmake/gflags/gflags-config.cmake | CMake 包配置 | find_package(gflags) |
lib/cmake/gflags/gflags-targets.cmake | 导出的 imported target | target_link_libraries里写 target 名 |
提示:真正写进
DT_SONAME的是libgflags.so.2.1,不是libgflags.so.2.1.1。所以运行期报libgflags.so.2.1: cannot open shared object file时别去找带三个数字的文件,找的是那个中间软链。
这里还有一处容易踩的:2.1.x 的包配置导出的 target 名字是gflags和gflags_nothreads,没有gflags::gflags这种带双冒号的形式。你从别处抄来的 CMake 片段如果写target_link_libraries(app gflags::gflags),在 2.1.1 上会直接报 target 不存在,而在更新版本上又能跑。这个差异不是你的错,是包配置演进的锅。
2.4 三分钟验证:编一个最小程序
装完不验证,等于没装。写个二十行的 demo,把定义、解析、--help三件事一次跑通。
// demo_flags.cpp #include <cstdio> #include <gflags/gflags.h> DEFINE_string(model, "", "网络定义文件路径"); DEFINE_int32(gpu, -1, "GPU 序号,-1 表示 CPU 模式"); DEFINE_bool(use_cudnn, true, "是否启用 cuDNN"); int main(int argc, char** argv) { gflags::SetUsageMessage("demo --model=net.prototxt --gpu=0"); gflags::SetVersionString("0.1.0"); // 第三个参数 true:解析完把 flag 从 argv 里摘掉 gflags::ParseCommandLineFlags(&argc, &argv, true); printf("model=%s gpu=%d cudnn=%d\n", FLAGS_model.c_str(), FLAGS_gpu, FLAGS_use_cudnn); return 0; }PREFIX="$HOME/.local/gflags-2.1.1" g++ -std=c++11 demo_flags.cpp \ -I"$PREFIX/include" -L"$PREFIX/lib" \ -lgflags -lpthread -o demo # 动态库不在系统路径里,运行时得指一下 export LD_LIBRARY_PATH="$PREFIX/lib:$LD_LIBRARY_PATH" ./demo --model=deploy.prototxt --gpu=0 --nouse_cudnn ./demo --help # 看 usage + 全部 flag ./demo --version编译命令里-lpthread不能省。gflags 默认编的是带线程的版本,内部用了 mutex,静态链接时缺它会在链接期报一堆pthread_mutex_lock未定义。用gflags_nothreads变体才能绕开,但那样所有 flag 操作都不是线程安全的。
运行./demo --help会看到 usage 行、版本号、以及你定义的三个 flag 连同帮助文本和默认值——注意--help的退出码是 1 不是 0,写 CI 脚本判断成功与否的时候别把它当失败。
3. 命名空间这道坎:gflags 和 google 只能选一个
3.1 从头文件守卫看两代库的差别
老工程里经常能看到这么一段,看着莫名其妙,其实是在给两代 gflags 做兼容:
#include <gflags/gflags.h> // GFLAGS_GFLAGS_H_ 有定义 => 用的是新一代头文件,命名空间就叫 gflags // 没这个宏 => 用的是老一代头文件,命名空间叫 google,补个别名兜住 #ifndef GFLAGS_GFLAGS_H_ namespace gflags = google; #endif逻辑很直白:新一代头文件的包含守卫是GFLAGS_GFLAGS_H_,老一代是另一套。守卫在,说明命名空间已经是gflags,什么都不用做;守卫不在,说明拿到的是老头文件,里面只有google命名空间,那就补一个namespace gflags = google;的别名,让下面的gflags::ParseCommandLineFlags能编过。
这个写法在 Caffe 时代的工程里遍地都是,但它是编译期的补救,救不了链接期。如果库里编出来的符号是google::ParseCommandLineFlags,而你代码里经别名解析后也是google::,那没问题;可要是库是新命名空间、代码走的是别名指向google,链接器就找不到符号了。
注意:一个进程里只能存在一种命名空间的 gflags。gflags 库本身、你的业务代码、所有第三方依赖,三者必须一致。混着来一定会出现
undefined reference。
3.2 用 GFLAGS_NAMESPACE 重新编一份 google 版
如果整条依赖链上的代码都写死了google::,最省事的做法是让库跟着代码走,而不是去改几百个文件。
cd gflags-2.1.1/build # 清缓存,否则 GFLAGS_NAMESPACE 这种 CACHE 变量改了也不生效 rm -f CMakeCache.txt cmake .. \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX="$HOME/.local/gflags-2.1.1-google" \ -DBUILD_SHARED_LIBS=ON \ -DBUILD_TESTING=OFF \ -DGFLAGS_NAMESPACE=google make -j4 && make installGFLAGS_NAMESPACE是 CMake 的 CACHE 变量,一旦写进CMakeCache.txt就不会被新的-D覆盖,这是血泪经验:改了参数发现重编出来的符号没变,八成是忘了删缓存。改完装到另一个前缀也很关键——两个命名空间的版本装同一个目录,后装的会覆盖软链,前面编的程序直接崩。
验证方式不是看日志,是看符号:
PREFIX="$HOME/.local/gflags-2.1.1-google" # -D 只看动态符号表,-C 把 C++ 修饰名还原成人能读的形式 nm -DC "$PREFIX/lib/libgflags.so" | grep -E 'ParseCommandLineFlags|SetCommandLineOption' # 静态库同理,T 表示符号定义在代码段 nm -C "$PREFIX/lib/libgflags.a" | grep ' T ' | grep -i ParseCommandLineFlags | head -3输出应该是google::ParseCommandLineFlags(int*, char***, bool)。如果打出来是gflags::,说明缓存没清干净;如果什么都没有,说明 grep 的符号名不对或者库根本没装到那个路径。这一步比读任何文档都可靠,符号表不会骗人。
3.3 三种接法的取舍
| 做法 | 改哪里 | 好处 | 代价 |
|---|---|---|---|
编库时指定GFLAGS_NAMESPACE=google | 构建脚本 | 老代码一行不动 | 整个进程被锁死在google::,新写的模块也得跟着写 |
| 源码里加命名空间别名 | 每个用到 flag 的.cc | 不用重编库 | 容易漏,漏一个文件就报错;别名写错位置还会污染其他头文件 |
自建一层flags_compat.h包装 | 一个头文件 | 一处修改全局生效,将来升级只改这里 | 多一层间接,报错栈里多两帧 |
我一般选第三种:新建flags_compat.h,里面 include 真正的 gflags 头、做命名空间兼容、再统一#include到业务代码。这样将来从 2.1.1 换到别的版本,改的永远是一个文件。第二种看着省事,实际上项目一大就控制不住,我见过同一个工程里一半文件写gflags::、一半写google::,最后靠#define强行糊过去,半年后没人敢动。
4. 参数链路落地:定义、解析、运行期改写、外部配置
4.1 声明和定义必须分家
DEFINE_*展开后是一个全局变量定义,同一个 flag 在两个.cc里各写一次,链接期就是multiple definition。正确做法是头文件里DECLARE、源文件里DEFINE,全项目只有一份定义。
// model_flags.h:只声明,谁 include 都不会重复定义 #pragma once #include <gflags/gflags_declare.h> DECLARE_string(model); DECLARE_int32(gpu); DECLARE_bool(use_cudnn); DECLARE_double(threshold);// model_flags.cc:定义只能有一份 #include "model_flags.h" #include <gflags/gflags.h> DEFINE_string(model, "", "网络定义文件路径,必填"); DEFINE_int32(gpu, -1, "GPU 序号,-1 表示 CPU 模式"); DEFINE_bool(use_cudnn, true, "是否启用 cuDNN 加速"); DEFINE_double(threshold, 0.5, "分类阈值,取值 [0,1]");gflags_declare.h这个头文件值得单独用:它只有DECLARE_*宏,不带实现,头文件依赖轻。如果所有头文件都直接 includegflags.h,编译时间会慢慢堆起来——这不是玄学,一个中型工程能差出十几秒。
类型上 2.1.1 提供了DEFINE_bool/DEFINE_int32/DEFINE_int64/DEFINE_uint64/DEFINE_double/DEFINE_string六种。选型只看一个标准:这个值会不会超过 21 亿。GPU 编号、batch size 用int32;文件大小、字节偏移用int64;编号类永远不该是负数的用uint64,但要注意它和int32比较时的隐式转换坑。
本文还有配套的精品资源,点击获取