fmt 在 C++14 旧编译器下如何用 FMT_STRING 启用编译期格式字符串检查
【免费下载链接】fmtA modern formatting library项目地址: https://gitcode.com/GitHub_Trending/fm/fmt
如果你的项目还停留在 C++14(或未启用 C++20consteval的旧编译器),fmt 默认的编译期格式字符串检查不会被触发——因为该检查在 doc/api.md 中说明是"在支持 C++20consteval的编译器上默认启用"。这类编译器下对应的机制是 FMT_STRING 宏:把格式字符串字面量包进FMT_STRING(...),检查就会在编译期完成,而不是等到运行时抛出format_error。本文给出在 C++14 环境下使用FMT_STRING的操作路径,以及如何用FMT_ENFORCE_COMPILE_STRING强制所有调用点走编译期检查。
前提:确认你的编译走的是 legacy 检查路径
FMT_STRING在 fmt/format.h 中的定义取决于FMT_USE_CONSTEVAL:
#if FMT_USE_CONSTEVAL # define FMT_STRING(s) s #else # define FMT_STRING(s) FMT_STRING_IMPL(s, fmt::detail::compile_string) #endif // FMT_USE_CONSTEVAL也就是说,在旧编译器上(FMT_USE_CONSTEVAL为 0),FMT_STRING(s)展开为FMT_STRING_IMPL,格式字符串被构建成compile_string派生类型,检查在编译期执行;在支持consteval的新编译器上,FMT_STRING(s)就是字面量本身,检查由 C++20 机制完成。
FMT_USE_CONSTEVAL的判定逻辑在 fmt/core.h:未定义__cpp_lib_is_constant_evaluated、语言标准低于 C++20(FMT_CPLUSPLUS < 201709L,即 C++14/17 模式)、GCC < 10.2、Apple Clang < 14 等情况下都会置为 0。因此 C++14 编译环境下FMT_STRING必然走 legacy 路径。
注意边界:doc/api.md 明确FMT_STRING"requires C++14 or later and is a no-op in C++11"。如果你的工程只能编 C++11,这个宏不产生任何检查,需要把编译标准提升到 C++14 才有意义。
准备 fmt 库
按 doc/get-started.md 的任一方式准备即可,本文不重复展开,只列出与后续步骤直接相关的要点:
- 系统包安装:Debian/Ubuntu 用
apt install libfmt-dev,macOS 用brew install fmt,或conda install -c conda-forge fmt; - CMake 集成时链接
fmt::fmt(编译库)或fmt::fmt-header-only(header-only 模式),例如find_package(fmt)之后target_link_libraries(<your-target> fmt::fmt); - 编译选项保证 C++ 标准为 14 或更高(如
-std=c++14),这是FMT_STRING生效的前提。
用 FMT_STRING 包装格式字符串
FMT_STRING定义在fmt/format.h中,宏的文档注释给出了它的作用——"Constructs a legacy compile-time format string from a string literals",见 include/fmt/format.h。使用方式就是把格式字符串字面量直接包一层:
#include <fmt/format.h> int main() { // 检查在编译期完成 std::string s = fmt::format(FMT_STRING("The answer is {:d}"), 42); }fmt 官方测试 test/enforce-checks-test.cc 覆盖了各 API 的典型写法,可照搬为检查清单:
#include <iterator> #include <vector> #include <fmt/format.h> void test() { // fmt::format / fmt::print 等格式化函数 (void)fmt::format(FMT_STRING("{}"), 42); // 宽字符串字面量同样支持 (void)fmt::format(FMT_STRING(L"{}"), 42); // fmt::format_to std::vector<char> out; fmt::format_to(std::back_inserter(out), FMT_STRING("{}"), 42); // fmt::format_to_n char buffer[4]; fmt::format_to_n(buffer, 3, FMT_STRING("{}"), 12345); }两点限制需要注意:
FMT_STRING接受的是字符串字面量(宏实现中通过s[0]推导字符类型,见 FMT_STRING_IMPL),不能把运行时拼接的std::string塞进去;- 多余参数是允许的,"Unused arguments are allowed as in Python's
str.format"(doc/api.md 的 Compile-Time Checks 一节)。
验证:用一条错误的格式字符串确认检查生效
验证方式就是"故意写错,看编译器是否报错"。宏文档注释给出的示例正是这个场景:
// A compile-time error because 'd' is an invalid specifier for strings. std::string s = fmt::format(FMT_STRING("{:d}"), "foo");{:d}是整数说明符,传给字符串字面量"foo",预期结果是编译失败而不是运行期异常。对比一下:README 中的 C++20 示例fmt::format("{:d}", "I am not a number")"gives a compile-time error in C++20"——同样的错配在 C++20 下无需任何宏就能捕获,而 C++14 下必须包FMT_STRING才等价。
判定标准很直接:
- 错误用例(如上面的
{:d}+ 字符串)→ 编译期报错,检查生效; - 正确用例(如
FMT_STRING("The answer is {:d}"), 42)→ 正常编译,运行后得到格式化结果。
如果错误用例在你的工程里能编译通过,说明该编译单元没有启用 C++14(宏退化为 no-op),或者没有包含fmt/format.h中定义该宏的编译路径。
可选:用 FMT_ENFORCE_COMPILE_STRING 强制所有调用点
只靠自觉包FMT_STRING容易被漏。doc/api.md的 Legacy Compile-Time Checks 一节提供了开关:
// 必须在包含 fmt 头文件之前定义 #define FMT_ENFORCE_COMPILE_STRING #include <fmt/format.h> int main() { fmt::print("hello {}\n", 42); // 编译失败:普通字符串不被接受 fmt::print(FMT_STRING("hello {}\n"), 42); // 通过 }设置后,"functions acceptingFMT_STRINGwill fail to compile with regular strings"。实现上的强制点在 fmt/core.h 的enforce_compile_checks:违反时触发static_assert,信息为FMT_ENFORCE_COMPILE_STRING requires format strings to use FMT_STRING。适合在存量代码上一次性暴露所有未包装的格式字符串,改完后再评估是否保留该宏。
限制与边界
- C++11 下无检查:
FMT_STRING在 C++11 中是 no-op,编译标准必须 ≥ C++14。 - 只接受字面量:宏按
string literal构造compile_string类型,运行时格式字符串无法通过FMT_STRING获得编译期检查。 - C++20 下宏是恒等展开:
FMT_USE_CONSTEVAL为 1 时FMT_STRING(s)直接展开为s,检查转由 C++20consteval路径完成,行为与本文 C++14 路径等效,代码无需改动即可平滑过渡。
【免费下载链接】fmtA modern formatting library项目地址: https://gitcode.com/GitHub_Trending/fm/fmt
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考