inline関数やconst変数を使ってもC言語時代に#defineマクロによって実現していたことの全てをカバーできるわけではありません。
今でもCPP(C Pre Processor)の力は偉大です。
特に、ビルドレベルに応じて違う挙動になるような処理を実現するには#defineによるマクロは有効な選択です。
以下のような要件を満たすマクロを考えてみましょう
- 開発中のみ有効
- ある条件を与えて結果が偽ならログ出力
1 2 3 4 5 |
#ifdef _DEBUG #define DEBUG_LOG(exp, ...) do {if (!(exp)){std::printf(__VA_ARGS__);}} while(0) #else #define DEBUG_LOG(exp, ...) do {(void)(exp);} while(0) #endif |
以下のように使います。
1 2 3 4 5 |
int main() { int x = 99; DEBUG_LOG(x > 100,"warning! x = %d", x); // xが100以下だったら警告を表示 return 0; } |
DEBUG_LOGの定義を見て下さい。
マクロで展開したい実際の処理(std::printf)をdo{…}while(0)で囲んでます。
これはどういう意図でしょうか。
do{…}while(0)としたところで、内部のステートメントは1度しか実行されません。
do-while(0)の部分を省いて
1 |
#define DEBUG_LOG(exp,...) if ((!exp)){std::printf(__VA_ARGS__)} |
と書くのと処理的には何も変わらないように見えます。
しかし実際には後者の定義は
- 行末の;(セミコロン)を強制できない
- 記述する場所によっては意図しない挙動になる
- ビルドレベルによって記述できる箇所が変わる
という3点の理由により全く不十分なのです。
順番に見てみましょう。
1.行末の;(セミコロン)を強制できない
do-while(0)じゃないバージョンでは行末の;(セミコロン)を強制できません。
1 2 3 4 5 6 7 |
int main() { int x = 99; if (!(x > 100)) { std::printf("warning! x = %d", x); } return 0; } |
このように展開されるのでセミコロンが無くてもc++のシンタックス上は問題の無いコードになります。(3-5行目)
しかしエディタ上での見た目は
1 2 3 4 5 |
int main() { int x = 99; DEBUG_LOG(x > 100,"warning! x = %d", x) //←セミコロン無し return 0; } |
となります。これはc++のシンタックスに準じていないし、コード全体の一貫性も欠くことになります。しかし、そう書けてしまうというのは使う側ではなく、定義した側の責任です。
2.記述する場所によっては意図しない挙動になる
このマクロがブロック無しif文の直後で呼ばれた時に意図しない挙動になります。
1 2 3 4 |
if (p) DEBUG_LOG(x > 100, "warning!"); else p = nullptr; |
ブロック無しif文なんて書くな、カッコ悪いぞ!っていう話ではあるのですが、こう書いた時に以下のように展開されます。
1 2 3 4 5 6 |
if (p) if (!(x > 100)) { std::printf("warning!"); } else p = nullptr; |
if (p)と対だったはずのelse節がマクロから展開された方のif文との対になってしまいました。
これも使う側ではなく、定義した側の責任です。
do-whileで囲んだ最初の定義だと以下のように展開されます。
1 2 3 4 5 6 7 8 |
if (p) do { if (!(x > 100)) { std::printf("warning"); } } while(0); else p = nullptr; |
3.ビルドレベルによって記述できる箇所が変わる
マクロが展開を展開した時にシンタックスエラーになるケースが変わります。
1 2 3 4 |
DEBUG_LOG(x > 100, "warning!") else { //// なんか処理 } |
if文だけのマクロの場合はこのような書式がc++的に許されてしまいます。
しかもデバッグビルド時は問題無いのに、いざリリースしようとするとコンパイルエラーになってしまいます。
同じことが非デバッグ時のDEBUG_LOGの定義にも言え、例えば((void)0)を書かずに、展開結果をただ空白にしてしまうと、非デバッグビルド時には、どこにでも記述できるというマクロになってしまいます。
1 2 3 4 |
#else // リリース時には何にも展開されない #define DEBUG_LOG(exp, ...) #endif |
1 2 3 |
int main(DEBUG_LOG(1)) { return DEBUG_LOG(0) 0; } |
このように書いてもエラーになりません。
マクロはc++のシンタックスを無視して、どこにでも記述できてしまいます。どこにでも記述させたい明確な理由が無い場合、このような記述はできるべきではありませんし、そもそもデバッグビルド時と記述可能箇所のルールが変わってしまいます。
ビルドレベルによって記述可能箇所のルールが変わるのは、単に分かりづらい上に、ビルドレベルを変えたタイミングで、初めてコーディングのミスに気づくことになるので効率も悪いです。
どういうスキルレベルのプログラマに使われても不具合が起こりにくくなるように、可能な限り、紛れの無いコーディングを促すような設計を心がけましょう。
まとめ
- 展開された時に意図しない挙動になるのを防ぐ為、マクロをdo-while(0)でくくるのは良いプラクティス
- ビルドレベルが変わってもマクロを展開できる箇所には一貫性をもたせるべき