这是我最喜欢的
#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
(我 suggest 没有使用 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) 的东西之后遇到了this gem,你真的也想要这些宏中的所有花哨!
希望我的示例可以提供一些洞察力,让您了解至少可以做一些聪明的事情来改进您的 C++ 代码。在编写代码时检测代码确实很有价值,而不是在您不了解正在发生的事情时回来执行它。但是,您必须在使其健壮和按时完成之间取得平衡。
我喜欢将额外的调试构建健全性检查视为工具箱中的不同工具,类似于单元测试。在我看来,它们可能会更强大,因为与其将您的健全性检查逻辑放在单元测试中并将它们与实现隔离,如果它们包含在实现中并且可以随意变出,那么完整的测试就不是必需的了因为您可以在紧要关头简单地启用检查并照常运行。