C++如何实现简易的断言宏支持日志?(自定义assert)

4次阅读

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

C++如何实现简易的断言宏支持日志?(自定义assert)

为什么标准 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_valauto 避免类型硬编码,兼容 intbool指针
  • #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。标准 assertNDEBUG 下消失,但你的 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)或模板特化,而不是单纯删宏。

text=ZqhQzanResources