定义易维护函数出入日志器:如何免宏实现?或宏是否适用?
我想要实现一个函数出入日志器,核心目标有两个:
- 无需在每个函数中显式引用
__func__这类隐式变量(比如后续切换为GCC的__PRETTY_FUNCTION__时不用大量修改代码); - 尽量避免使用宏。
但目前我没办法达成第二个目标,当前的实现是通过宏结合RAII类来完成的:
#define LOG_ENTRY_EXIT() entry_exit_logger e(__func__); using func_name_type = const char*; class entry_exit_logger{ public: entry_exit_logger(const func_name_type fname) : func_name(fname){ std::cout << "-------Enter: " << func_name << "------" << std::endl; } ~entry_exit_logger(){ std::cout << "-------Exit: " << func_name << "------" << std::endl; } private: const func_name_type func_name; }; void test_entry_exit(){ LOG_ENTRY_EXIT(); } int main(){ test_entry_exit(); return 0; }
想请教一下,有没有办法不使用宏来实现这个日志器?或者说,这种场景其实更适合使用宏?
先直接给你结论:这个场景其实非常适合用宏,但如果实在想规避宏,也有一些(代价不小的)替代方案。
为什么这个场景适合用宏?
宏是少数能在调用点自动捕获__func__、__PRETTY_FUNCTION__这类编译器隐式变量的工具——这些变量的作用域是当前函数,只有在函数内部直接展开的代码才能拿到正确的值。你的第一个需求是"后续切换这类变量不用大量修改",宏刚好能完美满足:只需要修改宏定义里的变量名,所有调用点都会自动更新,完全不需要改动业务代码。
而且宏的写法非常简洁,业务函数里只需要一行LOG_ENTRY_EXIT()就能完成日志注入,对代码侵入性极低。
如果硬要规避宏,有哪些替代方案?
这些方案都有明显的缺点,你需要权衡是否接受:
模板函数 + 手动传递函数名
你可以写一个模板函数返回RAII对象,但必须手动在每个函数里传入__func__:using func_name_type = const char*; class entry_exit_logger{ public: entry_exit_logger(const func_name_type fname) : func_name(fname){ std::cout << "-------Enter: " << func_name << "------" << std::endl; } ~entry_exit_logger(){ std::cout << "-------Exit: " << func_name << "------" << std::endl; } private: const func_name_type func_name; }; template<typename FuncName> auto make_entry_exit_logger(FuncName&& fname) { return entry_exit_logger(std::forward<FuncName>(fname)); } void test_entry_exit(){ auto logger = make_entry_exit_logger(__func__); }缺点很明显:违背了你第一个需求的初衷——后续要切换
__PRETTY_FUNCTION__时,你得逐个修改所有函数里的参数,工作量巨大。利用C++20的
std::source_location
C++20引入了std::source_location,可以在函数里获取调用点的信息,包括函数名:#include <source_location> using func_name_type = const char*; class entry_exit_logger{ public: entry_exit_logger(const std::source_location& loc = std::source_location::current()) : func_name(loc.function_name()){ std::cout << "-------Enter: " << func_name << "------" << std::endl; } ~entry_exit_logger(){ std::cout << "-------Exit: " << func_name << "------" << std::endl; } private: const func_name_type func_name; }; void test_entry_exit(){ entry_exit_logger logger; }这个方案看起来完美:不需要宏,也不用手动传函数名,后续如果需要调整获取函数名的方式,只需要修改
entry_exit_logger的构造函数即可。但它有两个限制:- 必须使用C++20及以上的标准;
std::source_location::function_name()返回的函数名格式可能和__func__、__PRETTY_FUNCTION__略有不同,比如会包含更多的命名空间信息,需要确认是否符合你的日志格式要求。
总结
如果你的项目可以使用C20,std::source_location是替代宏的最佳方案;如果只能用C17及以下,那宏就是最简洁、最符合你需求的选择——完全没必要为了规避宏而增加代码维护成本。毕竟宏在这种"捕获编译期隐式变量+统一修改入口"的场景下,是C++里最顺手的工具之一。
内容的提问来源于stack exchange,提问作者curios_case

