标准 assert 不能直接打日志,因其失败时调用 abort() 且不输出文件名、行号、表达式原文等关键调试信息;需封装 log_assert 宏,用 do-while 和逗号表达式确保线程安全、不重复求值,并支持自定义消息与 release 级别配置。

为什么标准 assert 不能直接打日志
标准 assert 在失败时调用 abort(),不输出上下文,也不走你的日志系统。它甚至不保留文件名、行号、表达式原文——这些恰恰是调试时最需要的信息。
真正能用的日志断言,得自己封装一层,把触发条件、位置、可读描述都塞进日志管道里。
-
assert展开后是宏,不是函数,没法加日志调用逻辑 - 宏展开发生在预处理阶段,
__FILE__和__LINE__是可靠的,但__func__在某些旧编译器(如 GCC 4.7 之前)可能不可用 - 别试图在宏里调用带副作用的函数(比如
log_error(...)多次求值),表达式必须只执行一次
怎么写一个线程安全、不重复求值的 LOG_ASSERT
核心是用逗号表达式 + do {...} while(0) 结构,确保语义完整且可放在任何语句位置。关键点:先求值,再判断,失败才打日志。
#define LOG_ASSERT(expr) \
do { \
auto _expr_val = (expr); \
if (!_expr_val) { \
log_error("ASSERT failed: {} at {}:{} ({})", #expr, __FILE__, __LINE__, __func__); \
std::abort(); \
} \
} while(0)
-
_expr_val用auto避免类型硬编码,兼容int、bool、指针等 -
#expr把原始表达式转成字符串,比只打"expr"有用得多 - 如果日志函数不是
noexcept,且你启用了异常(比如log_error内部抛异常),std::abort()前可能已崩溃——这时优先保证 abort 不被绕过 - Windows 下若用
OutputDebugString替代log_error,注意字符串需为 UTF-16,得转换
如何让 LOG_ASSERT 支持自定义消息
标准 assert 没消息字段,但实际开发中“空指针”和“超时未响应”需要不同提示。C++20 的 static_assert 支持字符串字面量,但运行时 assert 不行——只能靠宏重载。
立即学习“C++免费学习笔记(深入)”;
用 GCC/Clang 的变参宏扩展(C99 标准,C++11 起广泛支持):
#define LOG_ASSERT(...) LOG_ASSERT_IMPL(__VA_ARGS__)
#define LOG_ASSERT_IMPL(expr, ...) \
do { \
auto _expr_val = (expr); \
if (!_expr_val) { \
log_error("ASSERT failed: {} at {}:{} ({}) — {}", #expr, __FILE__, __LINE__, __func__, ##__VA_ARGS__); \
std::abort(); \
} \
} while(0)
- 调用形式:
LOG_ASSERT(ptr != nullptr, "ptr is null after init") -
##__VA_ARGS__是 GCC 扩展,用于处理零参数情况(避免末尾逗号),MSVC 需用__VA_OPT__(C++20)或单独写两个宏 - 如果日志库不支持格式化(比如只接受
const char*),就别拼接字符串,改用多参数日志接口,或提前snprintf到栈缓冲区
Release 构建下要不要关掉 LOG_ASSERT
要,但别简单用 #ifdef NDEBUG。标准 assert 在 NDEBUG 下消失,但你的 LOG_ASSERT 如果也这样,会导致测试环境通过、线上崩溃——因为某些“断言”其实在做必要校验(比如指针非空、数组索引合法)。
- 区分两类行为:
DEBUG_ASSERT(仅调试)和ENSURE(生产环境也要检查,失败则记录并返回错误码) - 不要让
LOG_ASSERT在 Release 下静默失效;至少保留求值+日志,去掉abort(),或换成可配置的回调(如on_assert_fail) - 如果项目用 CMake,可通过
target_compile_definitions(mylib PRIVATE LOG_ASSERT_LEVEL=1)控制粒度,0=关闭,1=日志不 abort,2=日志+abort
最容易被忽略的是:宏里的表达式在 Release 下是否仍会被编译器优化掉?答案是否定的——只要宏展开后代码存在,即使没分支逻辑,(ptr->data) 这种解引用仍会触发空指针访问。所以,真要关,得用 if constexpr(C++17)或模板特化,而不是单纯删宏。










