75

我刚刚在 C 中遇到了一个我非常喜欢的 DEBUG 宏

#ifdef DEBUG_BUILD
#  define DEBUG(x) fprintf(stderr, x)
#else
#  define DEBUG(x) do {} while (0)
#endif

我猜 C++ 类似物是:-

#ifdef DEBUG_BUILD
#  define DEBUG(x) cerr << x
#else
#  define DEBUG(x) do {} while (0)
#endif
  1. 第二个代码片段是否类似于 C 中的代码片段?
  2. 你有最喜欢的 C++ 调试宏吗?

编辑:“调试宏”是指“在调试模式下运行程序时可能派上用场的宏”。

4

10 回答 10

51

第二个代码片段是否类似于 C 中的代码片段?

或多或少。它更强大,因为您可以<<在参数中包含 - 分隔值,因此使用单个参数您会得到需要 C 中可变数量的宏参数的东西。另一方面,人们滥用的可能性很小它通过在参数中包含一个分号。甚至会因为调用后忘记分号而遇到错误。所以我会把它包含在一个 do 块中:

#define DEBUG(x) do { std::cerr << x; } while (0)

你有最喜欢的 C++ 调试宏吗?

我喜欢上面的那个并且经常使用它。我的无操作通常只是阅读

#define DEBUG(x)

这对于优化编译器具有相同的效果。尽管下面@Tony D 的评论是正确的:这可能会导致一些语法错误未被检测到。

我有时也包括运行时检查,从而提供某种形式的调试标志。正如@Tony D 提醒我的那样,里面有一个 endl 通常也很有用。

#define DEBUG(x) do { \
  if (debugging_enabled) { std::cerr << x << std::endl; } \
} while (0)

有时我还想打印表达式:

#define DEBUG2(x) do { std::cerr << #x << ": " << x << std::endl; } while (0)

在某些宏中,我喜欢包含__FILE__, __LINE__or __func__,但这些通常是断言而不是简单的调试宏。

于 2013-01-10T10:56:47.287 回答
40

这是我最喜欢的

#ifdef DEBUG 
#define D(x) x
#else 
#define D(x)
#endif

它非常方便,并且可以生成干净的(重要的是,在发布模式下快速!!)代码。

到处都是很多#ifdef DEBUG_BUILD块(过滤掉与调试相关的代码块)非常难看,但当你用D().

如何使用:

D(cerr << "oopsie";)

如果这对你来说仍然太丑/奇怪/太长,

#ifdef DEBUG
#define DEBUG_STDERR(x) (std::cerr << (x))
#define DEBUG_STDOUT(x) (std::cout << (x))
//... etc
#else 
#define DEBUG_STDERR(x)
#define DEBUG_STDOUT(x)
//... etc
#endif

(我建议不要使用,using namespace std;虽然可能using std::cout; using std::cerr;是个好主意)

请注意,当您考虑“调试”时,您可能想做的不仅仅是打印到 stderr。发挥创造力,您可以构建能够深入了解程序中最复杂交互的结构,同时允许您快速切换到构建不受调试工具影响的超高效版本。

例如,在我最近的一个项目中,我有一个巨大的仅调试块,它开始FILE* file = fopen("debug_graph.dot");并继续以点格式转储一个与graphviz兼容的图,以可视化我的数据结构中的大树。更酷的是 OS X graphviz 客户端会在文件发生变化时自动从磁盘读取文件,因此只要程序运行,图形就会刷新!

我还特别喜欢使用仅调试成员和函数来“扩展”类/结构。这开启了实现功能和状态的可能性,这些功能和状态可以帮助您跟踪错误,就像调试宏中包含的所有其他内容一样,通过切换构建参数来删除。一个庞大的例程,在每次状态更新时都煞费苦心地检查每个角落案例?不是问题。拍拍D()它。一旦你看到它工作,-DDEBUG从构建脚本中删除,即为发布而构建,它就消失了,随时可以重新启用你的单元测试或你有什么。

一个大的,有点完整的例子,来说明(可能有点过分热心)这个概念的使用:

#ifdef DEBUG
#  define D(x) x
#else
#  define D(x)
#endif // DEBUG

#ifdef UNITTEST
#  include <UnitTest++/UnitTest++.h>
#  define U(x) x // same concept as D(x) macro.
#  define N(x)
#else
#  define U(x)
#  define N(x) x // N(x) macro performs the opposite of U(x)
#endif

struct Component; // fwd decls
typedef std::list<Component> compList;

// represents a node in the graph. Components group GNs
// into manageable chunks (which turn into matrices which is why we want
// graph component partitioning: to minimize matrix size)
struct GraphNode {
    U(Component* comp;) // this guy only exists in unit test build
    std::vector<int> adj; // neighbor list: These are indices
    // into the node_list buffer (used to be GN*)
    uint64_t h_i; // heap index value
    U(int helper;) // dangling variable for search algo to use (comp node idx)
    // todo: use a more space-efficient neighbor container?
    U(GraphNode(uint64_t i, Component* c, int first_edge):)
    N(GraphNode(uint64_t i, int first_edge):)
        h_i(i) {
        U(comp = c;)
        U(helper = -1;)
        adj.push_back(first_edge);
    }
    U(GraphNode(uint64_t i, Component* c):)
    N(GraphNode(uint64_t i):)
        h_i(i)
    {
        U(comp=c;)
        U(helper=-1;)
    }
    inline void add(int n) {
        adj.push_back(n);
    }
};

// A component is a ugraph component which represents a set of rows that
// can potentially be assembled into one wall.
struct Component {
#ifdef UNITTEST // is an actual real struct only when testing
    int one_node; // any node! idx in node_list (used to be GN*)
    Component* actual_component;
    compList::iterator graph_components_iterator_for_myself; // must be init'd
    // actual component refers to how merging causes a tree of comps to be
    // made. This allows the determination of which component a particular
    // given node belongs to a log-time operation rather than a linear one.

    D(int count;) // how many nodes I (should) have

    Component(): one_node(-1), actual_component(NULL) {
        D(count = 0;)
    }
#endif
};

#ifdef DEBUG
// a global pointer to the node list that makes it a little
// easier to reference it
std::vector<GraphNode> *node_list_ptr;

#  ifdef UNITTEST
std::ostream& operator<<(std::ostream& os, const Component& c) {
    os << "<s=" << c.count << ": 1_n=" << node_list_ptr->at(c.one_node).h_i;
    if (c.actual_component) {
        os << " ref=[" << *c.actual_component << "]";
    }
    os << ">";
    return os;
}
#  endif
#endif

请注意,对于大块代码,我只使用常规块#ifdef条件,因为这在一定程度上提高了可读性,而对于大块,使用极短的宏更是一个障碍!

宏必须存在的原因是指定禁用单元测试时N(x)添加的内容。

在这部分:

U(GraphNode(uint64_t i, Component* c, int first_edge):)
N(GraphNode(uint64_t i, int first_edge):)

如果我们能说类似的话就好了

GraphNode(uint64_t i, U(Component* c,) int first_edge):

但我们不能,因为逗号是预处理器语法的一部分。省略逗号会产生无效的 C++ 语法。

如果您在编译调试时有一些额外的代码,您可以使用这种类型的相应逆调试宏。

现在这段代码可能不是“真正好的代码”的例子,但它说明了一些你可以通过巧妙地应用宏来完成的事情,如果你保持纪律,不一定是邪恶的。

我刚刚在想知道这些东西之后发现了这个宝石do{} while(0),你真的也想要这些宏中的所有幻想!

希望我的示例可以提供一些洞察力,至少可以了解一些可以用来改进 C++ 代码的巧妙方法。在编写代码时检测代码确实很有价值,而不是在您不了解正在发生的事情时回来执行它。但是,您必须在使其健壮和按时完成之间取得平衡。

我喜欢将额外的调试构建健全性检查视为工具箱中的不同工具,类似于单元测试。在我看来,它们可能会更强大,因为与其将您的健全性检查逻辑放在单元测试中并将它们与实现隔离,如果它们包含在实现中并且可以随意变出,那么完整的测试就不是必需的了因为您可以在紧要关头简单地启用检查并照常运行。

于 2013-01-10T05:18:30.767 回答
9

对于问题 1] 答案是肯定的。它只会将消息打印到标准错误流。

对于问题2]有很多。我的最爱是

#define LOG_ERR(...) fprintf(stderr, __VA_ARGS__)

这将允许在调试消息中包含任意数量的变量。

于 2013-01-10T05:22:36.400 回答
8

我喜欢使用带有__LINE__,的宏__FILE__作为参数来显示打印输出代码中的位置 - 在多个位置打印相同的变量名称并不少见,所以fprintf(stderr, "x=%d", x);如果你再添加另一个相同的十行,那就没什么意义了向下。

我还使用了覆盖某些函数并存储调用它的位置(例如内存分配)的宏,以便稍后我可以弄清楚是哪一个泄漏了。对于内存分配,这在 C++ 中有点困难,因为您倾向于使用 new/delete,并且它们不容易被替换,但是锁定/解锁操作等其他资源可以非常有用地跟踪这种方式 [当然,如果您有一个像优秀的 C++ 程序员一样使用构造/破坏的锁定包装器,您可以将其添加到构造函数中,以便在获得锁后将文件/行添加到内部结构中,并且您可以看到它在其他地方的位置你无法在某处获得它]。

于 2013-01-10T09:26:58.610 回答
7

这是我目前使用的日志宏:

#ifndef DEBUG 
#define DEBUG 1 // set debug mode
#endif

#if DEBUG
#define log(...) {\
    char str[100];\
    sprintf(str, __VA_ARGS__);\
    std::cout << "[" << __FILE__ << "][" << __FUNCTION__ << "][Line " << __LINE__ << "] " << str << std::endl;\
    }
#else
#define log(...)
#endif

用法:

log(">>> test...");

输出:

xxxx/proj.ios_mac/Classes/IntroScene.cpp][gotoNextScene][Line 58] >>> test...
于 2015-07-01T04:44:02.453 回答
5

…并作为所有回复的附录:

就我个人而言,我从不使用宏DEBUG来区分调试和发布代码,而是使用NDEBUG必须发布版本定义的宏来消除assert()调用(是的,我assert()广泛使用)。如果后者没有定义,那么它是一个调试版本。简单的!所以,实际上没有理由再引入一个调试宏!DEBUG(并在NDEBUG两者都未定义时处理可能的情况)。

于 2015-06-04T13:51:07.377 回答
4

这是我的版本,使用可变参数模板print函数:

template<typename... ArgTypes>
inline void print(ArgTypes... args)
{
  // trick to expand variadic argument pack without recursion
  using expand_variadic_pack = int[];
  // first zero is to prevent empty braced-init-list
  // void() is to prevent overloaded operator, messing things up
  // trick is to use the side effect of list-initializer to call a function
  // on every argument.
  // (void) is to suppress "statement has no effect" warnings
  (void)expand_variadic_pack{0, ((cout << args), void(), 0)... };
}

#ifndef MYDEBUG
#define debug_print(...)
#else
#define debug_print(...) print(__VA_ARGS__)
#endif

我制作的版本是debug_print一个可变参数模板函数,它接受一个调试级别,允许我选择我想在运行时输出什么样的输出:

template<typename... ArgTypes>
inline void debug_print(debug::debug level, ArgTypes... args)
{
  if(0 != (debug::level & level))
    print(args...);
}

请注意该print函数会导致 Visual Studio 2013 Preview 崩溃(我尚未测试 RC)。我注意到它比我以前使用ostream重载的子类的解决方案更快(在 Windows 上,控制台输出很慢) operator<<

如果您只想调用一次真正的输出函数(或编写自己的类型安全;-)),也可以使用临时stringstream内部printprintf

于 2013-09-30T15:48:36.570 回答
3

我使用下面的代码进行日志记录。有几个优点:

  1. 我可以在运行时打开/关闭它们。
  2. 我可以编译出特定日志级别的语句。例如,目前,我在KIMI_PRIVATE宏中无条件编译,因为我正在调试发布版本中的某些内容,但由于记录了很多潜在的秘密酱料(大声笑),我从发布版本中编译它。

多年来,这种模式对我很有帮助。注意:虽然有一个全局logMessage函数,但代码通常会将日志排队到日志线程。

#define KIMI_LOG_INTERNAL(level,EXPR)           \
  if(kimi::Logger::loggingEnabled(level))       \
  {                                             \
    std::ostringstream os;                      \
    os << EXPR;                                 \
    kimi::Logger::logMessage(level ,os.str());  \
  }                                             \
  else (void) 0

#define KIMI_LOG(THELEVEL,EXPR)                 \
  KIMI_LOG_INTERNAL(kimi::Logger::LEVEL_ ## THELEVEL,EXPR)

#define KIMI_ERROR(EXPR)   KIMI_LOG(ERROR,EXPR)
#define KIMI_VERBOSE(EXPR) KIMI_LOG(VERBOSE,EXPR)
#define KIMI_TRACE(EXPR)   KIMI_LOG(TRACE,EXPR)
#define KIMI_INFO(EXPR)    KIMI_LOG(INFO,EXPR)
#define KIMI_PROFILE(EXPR) KIMI_LOG(TRACE,EXPR)

// Use KIMI_PRIVATE for sensitive tracing
//#if defined(_DEBUG)
#  define KIMI_PRIVATE(EXPR) KIMI_LOG(PRIVATE,EXPR)
// #else
// #  define KIMI_PRIVATE(EXPR) (void)0
// #endif
于 2013-06-03T04:42:52.923 回答
2

我使用以下微,

#if DEBUG
#define LOGE2(x,y) std::cout << "ERRO : " << "[" << __FILE__ << "][" << __FUNCTION__ << "][Line " << __LINE__ << "] " << x <<":"<< y <<std::endl;
#define LOGI2(x,y) std::cout << "INFO : " << "[" << __FILE__ << "][" << __FUNCTION__ << "][Line " << __LINE__ << "] " << x <<":"<< y << std::endl;
#define LOGD2(x,y) std::cout << "DEBG : " << "[" << __FILE__ << "][" << __FUNCTION__ << "][Line " << __LINE__ << "] " << x <<":"<< y << std::endl;
#define LOGE(x) std::cout << "ERRO : " << "[" << __FILE__ << "][" << __FUNCTION__ << "][Line " << __LINE__ << "] " << x << std::endl;
#define LOGI(x) std::cout << "INFO : " << "[" << __FILE__ << "][" << __FUNCTION__ << "][Line " << __LINE__ << "] " << x << std::endl;
#define LOGD(x) std::cout << "DEBG : " << "[" << __FILE__ << "][" << __FUNCTION__ << "][Line " << __LINE__ << "] " << x << std::endl;
#else
#define LOGE2(x,y) NULL
#define LOGI2(x,y) NULL
#define LOGD2(x,y) NULL
#define LOGE(x) NULL
#define LOGI(x) NULL
#define LOGD(x) NULL
#endif

采用:

LOGE("ERROR.");
LOGE2("ERROR1","ERROR2");
于 2017-02-01T14:54:39.683 回答
0

从其他答案中可以清楚地看出,有很多。有一个我喜欢的,它既允许可变数量的参数,打印参数的名称,并且包括换行符。

void debug_print() { std::cerr << std::endl; }
template <typename Head, typename... Tail>
void debug_print(Head H, Tail... T) {
    std::cerr << ' ' << H;
    debug_print(T...);
}

#ifdef DEBUGFLAG
#  define DEBUG(...) std::cerr << "dbg  (" << #__VA_ARGS__ << "):", \
                     debug_print(__VA_ARGS__)
#else
#  define DEBUG(...) do {} while(0)
#endif

大部分解释都与此答案相同,但我发现当我想在一行上调试打印多个变量而没有不同的宏时,额外的模板可以简化它。

于 2021-12-07T18:23:51.027 回答