gccな環境でいつも使ってるASSERTマクロ。printfが頼みの組み込みな環境で特に威力を発揮します。
#ifndef unlikely
# ifdef __builtin_expect
# define unlikely(x) __builtin_expect((x), 0)
# else
# define unlikely(x) (x)
# endif
#endif
#define ERROR(fmt, ...) do { \
fprintf(stderr, "\x1b[1;31mERROR:%s:%s:%d: " fmt "\x1b[0m", __BASE_FILE__, __FUNCTION__, __LINE__, ##__VA_ARGS__); \
} while (0)
#define ASSERT(b,action) ({ int __b = (int)(b); if(unlikely(!(__b))){ ERROR("failed (%s)\n", #b); action; } __b;})
// ASSERT(<条件式>, <失敗時アクション>)
ASSERT(maybe_failed() == SUCCESS, goto error);
この例だと失敗したときに↓を吐いてgoto error
が実行されます。
ERROR :main.c:main:17: failed (maybe_failed() == SUCCESS)
action
のところにはもちろんブロック式も書けます。
ASSERT(maybe_failed() == SUCCESS, { do_something(); goto error; });
- 失敗したときに任意のアクションを記述出来る
- 問題のあるソースの位置と評価式がプリントされて一目瞭然
- 失敗するとコンソールが赤くなるので危機感を煽れる
__builtin_expect
で最適化している(気分になれる)- コードの見た目が統一されて厳かになる
- returnコード方式でエラーを返していくとバックトレースが安く手に入る
- 評価式や関数名がリテラルで埋め込まれるので、バイナリが余分に大きくなる →リリースビルドでは詳細をダンプしない等で対応
- C0カバレッジで異常系がスルーされる傾向 →ある意味ではメリット
これ系では失敗した時の挙動を決め打ちしてしまうマクロを良く見かけるのですが、文をまるっと渡す事で柔軟性と規律を両立するこの書き方の方が個人的には好みです。
失敗する可能性のある処理の全てでこうした書き方を使うように習慣付けると、コードの防御力はかなり高まると思います。
(ちなみにそのままASSERT
だと他のフレームワークと衝突したりリリースビルドで消されそうに見えたりと、名前については一考の余地有り。)