web-dev-qa-db-ja.com

Cコンパイラがアサートする-実装方法は?

エラーの場合に、実行時に失敗するのではなく、コンパイルを防止する「アサート」を実装したいと思います。

私は現在、このように定義されていますが、うまく機能しますが、バイナリのサイズが大きくなります。

#define MY_COMPILER_ASSERT(EXPRESSION) switch (0) {case 0: case (EXPRESSION):;}

サンプルコード(コンパイルに失敗します)。

#define DEFINE_A 1
#define DEFINE_B 1
MY_COMPILER_ASSERT(DEFINE_A == DEFINE_B);

コードを生成しないようにこれを実装するにはどうすればよいですか(生成されるバイナリのサイズを最小限に抑えるため)?

42
NickB

純粋な標準Cでのコンパイル時のアサートが可能であり、プリプロセッサのちょっとしたトリックにより、その使用方法はassert()の実行時の使用方法と同じくらいきれいに見えます。

重要なトリックは、コンパイル時に評価でき、一部の値でエラーを引き起こす可能性のある構造を見つけることです。 1つの答えは、配列の宣言は負のサイズを持つことができないということです。 typedefを使用すると、成功した場合のスペースの割り当てが防止され、失敗した場合のエラーが保持されます。

エラーメッセージ自体は、負のサイズの宣言を暗号化して参照するため(GCCは「配列fooのサイズは負です」と言っています)、このエラーが本当にアサーションチェックであることを示唆する配列タイプの名前を選択する必要があります。

処理する別の問題は、特定の型名をコンパイルユニットで一度しかtypedefできないことです。したがって、マクロは宣言ごとに一意の型名を取得するために、各使用法を調整する必要があります。

私の通常の解決策は、マクロに2つのパラメーターがあることを要求することでした。 1つ目は、アサートする条件がtrueであり、2つ目は、裏で宣言された型名の一部です。台座による答えは、トークンの貼り付けと__LINE__事前定義されたマクロにより、追加の引数を必要とせずに一意の名前を形成できます。

残念ながら、アサーションチェックがインクルードファイルにある場合、2番目のインクルードファイルの同じ行番号、またはメインソースファイルのその行番号でチェックと衝突する可能性があります。マクロ__FILE__、しかしそれは文字列定数として定義されており、文字列定数を識別子名の一部に戻すことができるプリプロセッサトリックはありません。正当なファイル名には、識別子の正当な部分ではない文字を含めることができることは言うまでもありません。

したがって、次のコードの断片を提案します。

/** A compile time assertion check.
 *
 *  Validate at compile time that the predicate is true without
 *  generating code. This can be used at any point in a source file
 *  where typedef is legal.
 *
 *  On success, compilation proceeds normally.
 *
 *  On failure, attempts to typedef an array type of negative size. The
 *  offending line will look like
 *      typedef assertion_failed_file_h_42[-1]
 *  where file is the content of the second parameter which should
 *  typically be related in some obvious way to the containing file
 *  name, 42 is the line number in the file on which the assertion
 *  appears, and -1 is the result of a calculation based on the
 *  predicate failing.
 *
 *  \param predicate The predicate to test. It must evaluate to
 *  something that can be coerced to a normal C boolean.
 *
 *  \param file A sequence of legal identifier characters that should
 *  uniquely identify the source file in which this condition appears.
 */
#define CASSERT(predicate, file) _impl_CASSERT_LINE(predicate,__LINE__,file)

#define _impl_PASTE(a,b) a##b
#define _impl_CASSERT_LINE(predicate, line, file) \
    typedef char _impl_PASTE(assertion_failed_##file##_,line)[2*!!(predicate)-1];

一般的な使用法は次のようなものです。

#include "CAssert.h"
...
struct foo { 
    ...  /* 76 bytes of members */
};
CASSERT(sizeof(struct foo) == 76, demo_c);

GCCでは、アサーションエラーは次のようになります。

 $ gcc -c demo.c 
 demo.c:32:エラー:配列 `assertion_failed_demo_c_32 'のサイズが負です
 $ 
42
RBerteig

次のCOMPILER_VERIFY(exp)マクロは、かなりうまく機能します。

 //引数を組み合わせる(引数を展開した後)
#define GLUE(a、b)__GLUE(a、b)
#define __GLUE(a、b)a ## b 
 
#define CVERIFY(expr、msg)typedef char GLUE(compiler_verify_、msg)[(expr)? (+1):(-1)] 
 
#define COMPILER_VERIFY(exp)CVERIFY(exp、__LINE __)

これはCとC++の両方で機能し、typedefが許可される場所ならどこでも使用できます。式がtrueの場合、1文字の配列(無害)のtypedefが生成されます。式がfalseの場合、-1文字の配列のtypedefを生成します。これにより、通常、エラーメッセージが表示されます。 arugmentとして指定する式は、コンパイル時の定数に評価されるものであれば何でもかまいません(したがって、sizeof()を含む式は正常に機能します)。これにより、

#if(expr)
#error 
#endif 

ここでは、プリプロセッサーで評価できる式に制限されています。

7

コンパイラがDEBUGやNDEBUGのようなプリプロセッサマクロを設定する場合は、次のようなものを作成できます(それ以外の場合は、Makefileで設定できます)。

#ifdef DEBUG
#define MY_COMPILER_ASSERT(EXPRESSION)   switch (0) {case 0: case (EXPRESSION):;}
#else
#define MY_COMPILER_ASSERT(EXPRESSION)
#endif

次に、コンパイラはデバッグビルドに対してのみアサートします。

4
dreamlax

Cの静的アサーションで見つけられる最高の記事は pixelbeat です。静的アサーションがC++ 0Xに追加されており、C1Xに組み込まれる可能性がありますが、しばらくはそうではないことに注意してください。私が与えたリンクのマクロがあなたのバイナリのサイズを増やすかどうかはわかりません。少なくとも最適化の妥当なレベルでコンパイルした場合は、そうはならないのではないかと思いますが、走行距離はさまざまです。

4
ChrisInEdmonton

Cに興味があることは知っていますが、boostのC++ static_assert を見てください。 (ちなみに、これはC++ 1xで利用可能になる可能性があります。)

同様のことを、C++でも行いました。

#define COMPILER_ASSERT(expr)enum {ARG_JOIN(CompilerAssertAtLine、__LINE__)= sizeof(char [(expr)?+1:-1])} 

これはどうやらC++でのみ機能します。 この記事 は、Cで使用できるように変更する方法について説明しています。

4
leander

Leanderが言ったように、静的アサーションがC++ 11に追加され、現在は追加されています。

static_assert(exp, message)

例えば

#include "myfile.hpp"

static_assert(sizeof(MyClass) == 16, "MyClass is not 16 bytes!")

void doStuff(MyClass object) { }

cppreference page を参照してください。

3
Dylan

最終的なバイナリをコンパイルするときは、MY_COMPILER_ASSERTを空白に定義して、その出力が結果に含まれないようにします。デバッグ用の方法でのみ定義してください。

しかし、実際には、この方法ですべてのアサーションをキャッチすることはできません。コンパイル時に意味をなさないものもあります(値がnullではないという主張など)。他にできることは、他の#definesの値を確認することだけです。なぜそんなことをしたいのかよくわかりません。

2
Welbog

'#error'の使用は、ほとんどのコンパイラーでコンパイルを停止させる有効なプリプロセッサー定義です。たとえば、次のようにして、デバッグでのコンパイルを防ぐことができます。


#ifdef DEBUG
#error Please don't compile now
#endif
1
Steve Wranovsky

これは、GCCで最もわかりにくいエラーメッセージを提供するために見つかりました。他のすべてのものは、負のサイズまたは他の紛らわしいものについていくつかのサフィックスがありました:

#define STATIC_ASSERT(expr, msg)   \
typedef char ______Assertion_Failed_____##msg[1];  __unused \
typedef char ______Assertion_Failed_____##msg[(expr)?1:2] __unused

使用例:

 unsigned char testvar;
 STATIC_ASSERT(sizeof(testvar) >= 8, testvar_is_too_small);

そしてgccのエラーメッセージ(ARM/GNU Cコンパイラ:6.3.1):

conflicting types for '______Assertion_Failed_____testvar_is_too_small'
0
Joe