使用宏增加c++代码的详细程度

increase c++ code verbosity with macros

本文关键字:程度 代码 c++ 增加      更新时间:2023-10-16

为了调试程序,我希望能够增加详细程度。当然,我可以在运行时使用开关/标志来实现这一点。但是,由于我应该在代码中添加所有的"if"语句,这可能会非常低效。

因此,我想添加一个在编译过程中使用的标志,以便在代码中包含可选的、通常较慢的调试操作,而在不需要时不会影响程序的性能/大小。这里有一个例子:

/* code */
#ifdef _DEBUG_
/* do debug operations here 
#endif

因此,使用-D_DEBUG_进行编译就可以了。如果没有它,那部分就不会包含在我的程序中。

另一种选择(至少对于i/o操作)是至少定义一个i/o功能,如

#ifdef _DEBUG_
#define LOG(x) std::clog << x << std::endl;
#else
#define LOG(x) 
#endif

然而,我强烈怀疑这可能不是最干净的方法。那么,你会怎么做呢?

我更喜欢将#ifdef与实际函数一起使用,这样如果没有定义_DEBUG_,函数就会有一个空体:

void log(std::string x)
{
#ifdef _DEBUG_
  std::cout << x << std::endl;
#endif
}

这种偏好有三个主要原因:

  1. _DEBUG_没有定义时,函数定义是空的,任何现代编译器都会完全优化对该函数的任何调用(当然,定义应该在翻译单元中可见)
  2. #ifdef保护只需要应用于代码的一个小的本地化区域,而不是每次调用log
  3. 您不需要使用大量的宏,避免污染您的代码

您可以使用宏来更改函数的实现(类似于sftrabbit的解决方案)。这样,代码中就不会留下任何空的地方,编译器将优化"空"调用。

您还可以使用两个不同的文件进行调试和发布实现,并让您的IDE/构建脚本选择合适的一个;这根本不涉及CCD_ 6。只需记住DRY规则,并使干净的代码在调试场景中可重复使用即可。

我想说的是,他实际上非常依赖于你所面临的实际问题。有些问题将从第二种解决方案中受益更多,而简单的代码可能会更好地使用简单的定义。

您描述的这两个代码段都是使用条件编译通过编译时开关启用或禁用调试的正确方法。然而,您断言在运行时检查调试标志"可能非常低效,因为我应该向代码中添加所有的if语句",这一说法大多是不正确的:在大多数实际情况下,运行时检查不会以可检测的方式影响程序的速度,因此,如果保留运行时标志为您提供了潜在的优势(例如,在不重新编译的情况下打开调试来诊断生产中的问题),那么您应该选择运行时标志。

对于额外的检查,我将依赖于assert(请参阅assert.h),它正是您所需要的:在调试中编译时进行检查,在为发行版编译时不进行检查。

对于冗长的内容,您建议的C++版本将使用一个简单的Logger类,该类将布尔值作为模板参数。但是,如果将宏保存在Logger类中,则该宏也可以。

对于商业软件来说,在运行时在客户网站上提供一些调试输出通常是一件很有价值的事情。我并不是说所有的东西都必须编译成最终的二进制文件,但客户对你的代码做了你意想不到的事情(或者导致代码以你意想不到的方式运行)并不罕见。能够告诉客户"好吧,如果你运行myprog -v 2 -l logfile.txt并做你通常的事情,那么给我发电子邮件给logfile.txt"是一件非常有用的事情。

只要"如果决定我们是否登录的声明"不在秘鲁最深、最黑暗的丛林中,嗯,我的意思是在你的紧密、性能关键的循环中最深的嵌套层中,那么把它留在里面就很少有问题。

因此,我个人倾向于采用"永远在那里,而不是总是启用"的方法。这并不是说我有时不会发现自己在紧循环的中间添加一些额外的日志记录,只是在稍后修复错误时将其删除。

在进行条件编译时,可以避免类似宏的函数。只需定义一个常规或模板函数来进行日志记录,并在:中调用它

#ifdef _DEBUG_
/* ... */
#endif

代码的一部分。

至少在*Nix宇宙中,这类东西的默认定义是NDEBUG(读取无调试)。如果已定义,则代码应跳过调试代码。也就是说,你会做这样的事情:

#ifdef NDEBUG
inline void log(...) {}
#else
inline void log(...) { .... }
#endif

我在项目中使用的一段示例代码。通过这种方式,您可以使用变量参数列表,如果未设置DEBUG标志,则清除相关代码:

#ifdef DEBUG
#define PR_DEBUG(fmt, ...) 
    PR_DEBUG(fmt, ...) printf("[DBG] %s: " fmt, __func__, ## __VA_ARGS__)
#else
#define PR_DEBUG(fmt, ...)
#endif

用法:

#define DEBUG
<..>
ret = do_smth();
PR_DEBUG("some kind of code returned %d", ret);

输出:

[DBG] some_func: some kind of code returned 0

当然,printf()可能会被您使用的任何输出函数所取代。此外,它可以很容易地修改,以便自动附加附加信息,例如时间戳。

对我来说,它取决于不同的应用程序。

我有一些应用程序希望始终记录(例如,我们有一个应用程序,在出现错误的情况下,客户端会获取应用程序的所有日志,并将其发送给我们进行诊断)。在这种情况下,日志API可能应该基于函数(即,不是宏),并始终定义。

如果并非总是需要记录日志,或者出于性能/其他原因需要完全禁用日志,则可以定义日志宏。

在这种情况下,我更喜欢这样的单行宏:

#ifdef NDEBUG
#define LOGSTREAM /##/
#else
#define LOGSTREAM std::clog
// or
// #define LOGSTREAM std::ofstream("output.log", std::ios::out|std::ios::app)
#endif

客户端代码:

LOG << "Initializing chipmunk feeding module ...n";
//...
LOG << "Shutting down chipmunk feeding module ...n";

它和其他任何功能一样。

我的假设:

  • 没有全局变量
  • 系统设计为接口

对于任何需要详细输出的内容,创建两个实现,一个安静,一个详细。在应用程序初始化时,选择您想要的实现。

例如,它可以是记录器、小部件或内存管理器。

显然你不想重复代码,所以提取你想要的最小变化。如果你知道什么是策略模式,或者装饰模式,那么这些就是正确的方向。遵循打开-关闭的原则。