これが私のお気に入りです
#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 に出力するだけでなく、もっと多くのことをしたいかもしれないことに注意してください。創造力を働かせれば、プログラム内の最も複雑な相互作用への洞察を提供する構成を構築でき、同時に、デバッグ インストルメンテーションによって妨げられない非常に効率的なバージョンの構築に非常に迅速に切り替えることができます。
たとえば、私の最近のプロジェクトの 1 つで、データ構造内の大きなツリーを視覚化するために、Graphviz 互換のグラフをドット形式でダンプFILE* file = fopen("debug_graph.dot");
することから始まる巨大なデバッグ専用ブロックがありました。さらに優れているのは、OS X のグラフビズ クライアントは、ファイルが変更されたときにディスクからファイルを自動的に読み取るため、プログラムが実行されるたびにグラフが更新されることです。
また、デバッグ専用のメンバーと関数を使用してクラス/構造体を「拡張」することも特に好きです。これにより、バグの追跡に役立つ機能と状態を実装する可能性が開かれ、デバッグ マクロにラップされている他のすべてのものと同様に、ビルド パラメーターを切り替えることで削除されます。状態が更新されるたびに各コーナー ケースを念入りにチェックする巨大なルーチンですか? 問題ない。その周りを平手打ちし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++ コードを改善するために実行できる巧妙なことの少なくともいくつかについての洞察を提供できることを願っています。何が起こっているのか理解できないときに戻ってコードを実行するのではなく、コードを書いている間にコードを計測することは非常に価値があります。ただし、堅牢にすることと時間どおりに完了することとの間では、常にバランスを取る必要があります。
単体テストと同様に、追加のデバッグ ビルド サニティ チェックをツールボックス内の別のツールと考えるのが好きです。私の意見では、サニティ チェック ロジックを単体テストに入れて実装から分離するのではなく、それらが実装に含まれていて自由に呼び出すことができる場合、完全なテストはそれほど必要ではないため、それらはさらに強力になる可能性があります。ピンチで、チェックを有効にして通常どおりに実行できるためです。