这是我最喜欢的
#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++ 代码的巧妙方法。在编写代码时检测代码确实很有价值,而不是在您不了解正在发生的事情时回来执行它。但是,您必须在使其健壮和按时完成之间取得平衡。
我喜欢将额外的调试构建健全性检查视为工具箱中的不同工具,类似于单元测试。在我看来,它们可能会更强大,因为与其将您的健全性检查逻辑放在单元测试中并将它们与实现隔离,如果它们包含在实现中并且可以随意变出,那么完整的测试就不是必需的了因为您可以在紧要关头简单地启用检查并照常运行。