15

我希望有可能为我的程序的调试目的增加详细程度。当然,我可以在运行时使用开关/标志来做到这一点。但这可能非常低效,因为我应该将所有“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

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

4

10 回答 10

19

我更喜欢#ifdef与真正的函数一起使用,以便如果_DEBUG_未定义该函数有一个空主体:

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

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

  1. _DEBUG_未定义时,函数定义为空,任何现代编译器都会完全优化对该函数的任何调用(当然,该定义应该在该翻译单元内可见)。
  2. 守卫只需要应用于一小块本地化的#ifdef代码区域,而不是每次调用log.
  3. 您不需要使用大量的宏,避免了代码的污染。
于 2013-02-11T13:22:26.637 回答
2

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

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

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

于 2013-02-11T13:20:06.460 回答
2

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

于 2013-02-11T13:22:54.003 回答
1

对于其他检查,我将依赖断言(请参阅assert.h),它完全符合您的需要:在调试中编译时检查,在为发布编译时不检查。

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

于 2013-02-11T13:25:57.277 回答
1

对于商业软件,在运行时在客户站点上提供一些调试输出通常是一件很有价值的事情。我并不是说所有东西都必须编译成最终的二进制文件,但是客户对你的代码做你不期望的事情并不罕见[或者导致代码以你不期望的方式运行]。能够告诉客户“好吧,如果你跑步myprog -v 2 -l logfile.txt并做你平常的事情,然后给我发电子邮件logfile.txt”是一件非常非常有用的事情。

只要“决定我们是否登录的 if 语句”不在秘鲁最深、最黑暗的丛林中,嗯,我的意思是在您的紧密的、性能关键的循环的最深嵌套级别中,那么很少有问题把它留在里面。

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

于 2013-02-11T13:34:29.710 回答
0

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

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

部分代码。

于 2013-02-11T13:22:18.987 回答
0

至少在 *Nix 世界中,这种事情的默认定义是NDEBUG(阅读no-debug)。如果已定义,您的代码应跳过调试代码。即你会做这样的事情:

#ifdef NDEBUG
inline void log(...) {}
#else
inline void log(...) { .... }
#endif
于 2013-02-11T13:26:43.300 回答
0

我在项目中使用的示例代码。这样,您可以使用变量参数列表,如果未设置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()可以用您使用的任何输出函数代替。此外,它可以轻松修改,因此会自动附加附加信息,例如时间戳。

于 2013-02-11T13:24:26.037 回答
0

它就像任何其他功能一样。

我的假设:

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

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

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

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

于 2013-02-11T13:56:09.763 回答
0

对我来说,这取决于应用程序。

我有一些我想始终记录的应用程序(例如,我们有一个应用程序,如果出现错误,客户端会获取应用程序的所有日志并将它们发送给我们进行诊断)。在这种情况下,日志 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";
于 2013-02-11T13:38:17.283 回答